This commit is contained in:
吴红兵
2025-12-02 10:37:49 +08:00
commit 1f645dad3e
1183 changed files with 147673 additions and 0 deletions

View File

@@ -0,0 +1,229 @@
<template>
<ul class="vue-contextmenu-listWrapper"
@contextmenu.stop=""
:class="'vue-contextmenuName-' + props.contextMenuData.menuName">
<li v-for="item in props.contextMenuData.menulists"
class="context-menu-list"
:key="item.btnName">
<div v-if="item.children && item.children.length > 0" class="has-child">
<div class="parent-name btn-wrapper-simple"
:class="{'no-allow': item.disabled ? item.disabled : false}">
<i :class="item.icoName ? item.icoName : ''" class="nav-icon-fontawe" :style="item.icoStyle ? item.icoStyle : ''"></i>
<span class="nav-name-right">{{ item.btnName }}</span>
<i class="icon"></i>
</div>
<Tree :itemchildren="item.children" @childhandler="fnHandler" :float="data.floatDirection"/>
</div>
<div v-else>
<div class="node-connect" v-if="item.nodeConnect === true">
<div @click.stop="fnHandler(item)">
<img :src="item.icon" :alt="item.btnName">
<!-- <i :class="item.icoName ? item.icoName : ''"></i>-->
<span>{{item.btnName}}</span>
</div>
</div>
<div @click.stop="item.disabled === true ? '' : fnHandler(item)" v-else
class="no-child-btn btn-wrapper-simple"
:class="{'no-allow': item.disabled ? item.disabled : false}">
<i :class="item.icoName ? item.icoName : ''" class="nav-icon-fontawe" :style="item.icoStyle ? item.icoStyle : ''"></i>
<span class="nav-name-right">{{ item.btnName }}</span>
</div>
</div>
</li>
</ul>
</template>
<script setup lang="ts" name="VueContextMenu">
// 引入组件
const Tree = defineAsyncComponent(() => import('./tree.vue'));
const props = defineProps({
contextMenuData: {
type: Object,
required: false,
default() {
return {
menuName: null,
axis: {
x: null,
y: null
},
menulists: [
{
fnHandler: '',
icoName: '',
btnName: ''
}
]
}
}
},
transferIndex: {
type: Number,
default: 0
}
})
const data = reactive({
floatDirection: 'float-statue-1'
})
let menuLists = props.contextMenuData.menulists;
function contextMenuDataAxis(val) {
let x = val.x
let y = val.y
let innerWidth = window.innerWidth
let innerHeight = window.innerHeight
let index = props.transferIndex
let menuName = 'vue-contextmenuName-' + props.contextMenuData.menuName
let menu = document.getElementsByClassName(menuName)[index]
menu.style.display = 'block'
let menuHeight = menuLists.length * 30
let menuWidth = 150
menu.style.top = (y + menuHeight > innerHeight ? innerHeight - menuHeight : y) + 'px'
menu.style.left = (x + menuWidth > innerWidth ? innerWidth - menuWidth : x) + 'px'
/*document.addEventListener('mouseup', function (e) {
// 解决mac电脑在鼠标右键后会执行mouseup事件
if (e.button === 0) {
menu.style.display = 'none'
}
}, false)*/
if ((x + 2 * menuWidth) > innerWidth && (y + 2 * menuHeight) > innerHeight) {
data.floatDirection = 'float-status-4'
}
if ((x + 2 * menuWidth) < innerWidth && (y + 2 * menuHeight) > innerHeight) {
data.floatDirection = 'float-status-1'
}
if ((x + 2 * menuWidth) > innerWidth && (y + 2 * menuHeight) < innerHeight) {
data.floatDirection = 'float-status-3'
}
if ((x + 2 * menuWidth) < innerWidth && (y + 2 * menuHeight) < innerHeight) {
data.floatDirection = 'float-status-2'
}
}
let $emit = defineEmits(["flowInfo", "paste", "setNodeAttr", "copyNode", "deleteNode", "deleteLink", "setLinkAttr"
, "setSerialNode", "setParallelNode", "setSerialGate", "setParallelGate", "setConnectNode", "modifySourceNode", "modifyTargetNode"]);
function fnHandler(item) {
$emit(item.fnHandler)
}
// 监听双向绑定
watch(
() => props.contextMenuData.axis,
(val) => {
contextMenuDataAxis(val);
}
);
</script>
<style lang="scss" scoped>
.vue-contextmenu-listWrapper {
display: none;
position: fixed;
z-index: 2000;
background: #2c2c2c;
top: 0;
left: 0;
border-radius: 8px;
box-shadow: 0 2px 2px 0 #cccccc;
font-family: 'Courier New', Courier, monospace;
font-size: 12px;
padding-left: 10px !important;
color: white !important;
}
.vue-contextmenu-listWrapper .context-menu-list {
position: relative;
text-decoration: none;
list-style: none;
background: #2c2c2c;
margin: 10px 0;
}
.vue-contextmenu-listWrapper .node-connect {
div {
display: inline-block;
cursor: pointer;
width: 130px;
position: relative;
span {
position: absolute;
font-size: 14px;
margin-left: 16px;
margin-top: 3px;
}
i {
font-size: 25px;
border: 1px solid white;
border-radius: 15px;
margin-left: 10px;
}
img {
display: inline-block;
width: 25px;
height: 25px;
margin-left: 10px;
}
}
}
.context-menu-list:hover {
color: #6e6e6e;
}
.context-menu-list .has-child {
position: relative;
cursor: pointer;
padding: 5px 10px;
}
.context-menu-list:hover > .has-child > .child-ul-wrapper {
display: block;
}
.parent-name .icon {
position: absolute;
display: block;
top: 4px;
right: 0;
border-top: 4px solid transparent;
border-bottom: 4px solid transparent;
border-right: 4px solid transparent;
border-left: 8px solid white;
}
.no-child-btn {
//padding: 5px 10px;
cursor: pointer;
}
.nav-icon-fontawe {
position: absolute;
left: 0;
}
.nav-name-right {
margin: 0 20px;
//height: 16px;
line-height: 16px;
display: block;
}
.no-allow {
color: #d3cfcf;
cursor: not-allowed;
}
.btn-wrapper-simple {
position: relative;
//height: 16px;
line-height: 16px;
}
</style>

View File

@@ -0,0 +1,107 @@
<template>
<ul class="child-ul-wrapper" :class="props.float">
<li v-for="i in props.itemchildren" :key="i.name" class="child-li-wrapper">
<div v-if="i.children && i.children.length > 0" class="has-child">
<div class="parent-name btn-wrapper-simple">
<i :class="i.icoName ? i.icoName : ''" class="nav-icon-fontawe" :style="item.icoStyle ? item.icoStyle : ''"></i>
<span class="nav-name-right">{{i.btnName}}</span>
<i class="icon"></i></div>
<VueContextMenuTree :itemchildren="i.children" @childhandler="fnHandler" :float="props.float" />
</div>
<div v-else>
<div @click.stop="fnHandler(i)" class="no-child-btn btn-wrapper-simple">
<i :class="i.icoName ? i.icoName : ''" class="nav-icon-fontawe" :style="item.icoStyle ? item.icoStyle : ''"></i>
<span class="nav-name-right">{{i.btnName}}</span>
</div>
</div>
</li>
</ul>
</template>
<script setup lang="ts" name="VueContextMenuTree">
const props = defineProps({
itemchildren: [],
float: ''
})
let $emit = defineEmits(["flowInfo", "paste", "setNodeAttr", "copyNode", "deleteNode", "deleteLink", "setLinkAttr"
, "setSerialNode", "setParallelNode", "setSerialGate", "setParallelGate", "setConnectNode", "modifySourceNode", "modifyTargetNode"]);
function fnHandler (item) {
$emit('childhandler', item)
}
</script>
<style lang="scss" scoped>
.float-status-1 {
left: 100%;
bottom: -1px;
z-index: 10001
}
.float-status-2 {
left: 100%;
top: -1px;
z-index: 10001
}
.float-status-3 {
right: 100%;
top: -1px;
z-index: 10001
}
.float-status-4 {
right: 100%;
bottom: -1px;
z-index: 10001
}
.child-ul-wrapper .has-child {
padding: 5px 10px;
position: relative;
}
li {
list-style: none;
}
.parent-name .icon {
position: absolute;
display: block;
top: 4px;
right: 0;
border-top: 4px solid transparent;
border-left: 8px solid #111111;
border-bottom: 4px solid transparent;
border-right: 4px solid transparent;
}
.no-child-btn {
//padding: 5px 10px;
}
.child-ul-wrapper {
background: #ffffff;
position: absolute;
display: none;
border: 1px solid #e8e8e8;
border-radius: 3px;
}
.child-li-wrapper:hover > .has-child > .child-ul-wrapper{
display: block;
}
.context-menu-list:hover, .child-li-wrapper:hover {
background: #2894f8;
}
.nav-icon-fontawe {
position: absolute;
}
.nav-name-right {
white-space: nowrap;
display: block;
margin: auto 20px;
}
.btn-wrapper-simple {
position: relative;
//height: 16px;
line-height: 16px;
}
</style>

View File

@@ -0,0 +1,464 @@
import {ElMessage} from "element-plus";
import {validateNull} from "/@/utils/validate";
import {listDicUrlArr, listDicUrl} from "/@/api/jsonflow/common";
import {deepClone} from "/@/utils/other";
import {DIC_PROP} from "/@/flow/support/dict-prop";
import {useMessage} from "/@/hooks/message";
/**
* 远程请求数据字典工具类
*
* @author luolin
* @date 2023-04-25 16:41:52
*/
interface DicDataInterface {
key: string;
type?: string;
typeVal?: string;
field?: string;
method?: string;
dicUrl?: string;
prefix?: string;
cascades?: string[];
}
const dicArrUrls = {
listUsersByUserIds: "/jsonflow/user-role-auditor/list/user-ids",
listRolesByRoleIds: "/jsonflow/user-role-auditor/list/role-ids",
listPostsByPostIds: "/jsonflow/user-role-auditor/list/post-ids",
listDeptsByDeptIds: "/jsonflow/user-role-auditor/list/dept-ids",
listByFlowInstIds: "/jsonflow/run-flow/list/flow-inst-ids",
listByOrderIds: "/order/run-application/list/order-ids",
listByRunNodeIds: "/jsonflow/run-node/list/run-node-ids",
listByRunJobIds: "/jsonflow/run-job/list/run-job-ids",
}
/**
* 默认回调映射关系
*/
const dicArrFun = {
createUser: dicArrUrls.listUsersByUserIds,
handoverUser: dicArrUrls.listUsersByUserIds,
receiveUser: dicArrUrls.listUsersByUserIds,
initiatorId: dicArrUrls.listUsersByUserIds,
carbonCopy: dicArrUrls.listUsersByUserIds,
carbonCopyPerson: dicArrUrls.listUsersByUserIds,
userId: dicArrUrls.listUsersByUserIds,
delRoleId: dicArrUrls.listUsersByUserIds,
roleId: dicArrUrls.listUsersByUserIds,
permission: dicArrUrls.listRolesByRoleIds,
users: dicArrUrls.listUsersByUserIds,
roles: dicArrUrls.listRolesByRoleIds,
posts: dicArrUrls.listPostsByPostIds,
depts: dicArrUrls.listDeptsByDeptIds,
handoverUserDept: dicArrUrls.listDeptsByDeptIds,
receiveDept: dicArrUrls.listDeptsByDeptIds,
handleUserId: dicArrUrls.listUsersByUserIds,
parFlowInstId: dicArrUrls.listByFlowInstIds,
subFlowInstId: dicArrUrls.listByFlowInstIds,
flowInstId: dicArrUrls.listByFlowInstIds,
orderId: dicArrUrls.listByOrderIds,
runNodeId: dicArrUrls.listByRunNodeIds,
runJobId: dicArrUrls.listByRunJobIds,
fromRunNodeId: dicArrUrls.listByRunNodeIds,
toRunNodeId: dicArrUrls.listByRunNodeIds,
}
/**
* 表格数据加载完成后事件
*/
export const onLoaded = (...keyObjs: DicDataInterface[]) => {
return async (state: any, keyObjs2?: DicDataInterface) => {
let dataList = state.dataList;
if (dataList && dataList.length > 0) {
try {
let dicData = {}
let realKeyObjs = validateNull(keyObjs) ? {} : deepClone(keyObjs)
if (keyObjs2 && !validateNull(keyObjs2)) realKeyObjs = [deepClone(keyObjs2)]
for (const keyObj of realKeyObjs) {
let {key, type, typeVal, field, dicUrl} = keyObj;
if (!field) field = key
let keys = keyObjs2 ? dataList : dataList.filter(f => !type || f[type] === typeVal).map(m => m[field])
.flat().filter(f => !validateNull(f));
let ids = keys.filter((item, index) => {
return keys.indexOf(item, 0) === index;
});
if (validateNull(ids)) {
dicData[key] = []
continue;
}
if (!dicUrl) dicUrl = dicArrFun[key];
if (dicUrl.indexOf("?") !== -1) {
let params = dicUrl.split('?')[1].split('=');
dicUrl = dicUrl.replace("{{" + params[0] + "}}", typeVal)
}
let res = await listDicUrlArr(dicUrl, ids);
validateNull(res.data) ? dicData[key] = [] : dicData[key] = res.data;
}
Object.assign(state.dicData, dicData);
} catch (err: any) {
// 捕获异常并显示错误提示
ElMessage.error(err.msg || err.data.msg);
}
}
}
};
/**
* 表单数据加载完成后事件
*/
export const onFormLoadedUrl = (...keyObjs: DicDataInterface[]) => {
return async (dicData: any, form?: any, keyObjs2?: DicDataInterface) => {
try {
let realKeyObjs = validateNull(keyObjs) ? {} : deepClone(keyObjs)
if (keyObjs2 && !validateNull(keyObjs2)) realKeyObjs = [deepClone(keyObjs2)]
for (const keyObj of realKeyObjs) {
let {key, type, typeVal, field, dicUrl} = keyObj;
if (!field) field = key
let keys = validateNull(typeVal) ? form[field] : form[type] === typeVal ? form[field] : null;
if (form instanceof Array) {
keys = form.filter(f => !type || f[type] === typeVal).map(m => m[field])
.flat().filter(f => !validateNull(f));
}
if (!(keys instanceof Array)) keys = keys ? [keys] : []
let ids = keys.filter((item, index) => {
return keys.indexOf(item, 0) === index;
});
if (validateNull(ids)) {
// 参与者翻译需要
dicData[key] = []
continue;
}
if (!dicUrl) dicUrl = dicArrFun[key];
let res = await listDicUrlArr(dicUrl, ids);
validateNull(res.data) ? dicData[key] = [] : dicData[key] = res.data;
}
} catch (err: any) {
// 捕获异常并显示错误提示
ElMessage.error(err.msg || err.data.msg);
}
}
};
const cascadeUrls = {
listFlowNodeByDefFlowId: "/jsonflow/flow-node/list/def-flow-id/{{key}}",
listFlowNodeRelByDefFlowId: "/jsonflow/flow-node-rel/list/def-flow-id/{{key}}",
listNodeJobByFlowNodeId: "/jsonflow/node-job/list/flow-node-id/{{key}}",
listPreRunNodeByFlowInstId: "/jsonflow/run-node/flow-inst-id/pre/{{key}}?runNodeId={{runNodeId}}",
listAnyJumpRunNodeByFlowInstId: "/jsonflow/run-node/flow-inst-id/any-jump/{{key}}?runNodeId={{runNodeId}}",
listRunNodeByFlowInstId: "/jsonflow/run-node/list/flow-inst-id/{{key}}",
listUsersByRunNodeId: "/jsonflow/run-job/list/users/run-node-id/{{key}}",
listUserByRunNodeId: "/jsonflow/run-job/list/user/run-node-id/{{key}}",
listRoleByRunNodeId: "/jsonflow/run-job/list/role/run-node-id/{{key}}",
listPostByRunNodeId: "/jsonflow/run-job/list/post/run-node-id/{{key}}",
listDeptByRunNodeId: "/jsonflow/run-job/list/dept/run-node-id/{{key}}",
listRunJobByRunNodeId: "/jsonflow/run-job/list/run-job/run-node-id/{{key}}",
listDistRunJobByRunNodeId: "/jsonflow/run-job/list/dist-run-job/run-node-id/{{key}}",
getRunFlowByCode: "/jsonflow/run-flow/code/{{key}}",
getRunFlowByFlowInstId: "/jsonflow/run-flow/{{key}}",
listUserByRoleId: "/jsonflow/user-role-auditor/list-users/{{key}}?jobType={{jobType}}",
}
/**
* 默认回调映射关系
*/
const dicCascadeFun = {
flowNodeId: cascadeUrls.listFlowNodeByDefFlowId,
flowNodeRelId: cascadeUrls.listFlowNodeRelByDefFlowId,
nodeJobId: cascadeUrls.listNodeJobByFlowNodeId,
"runReject.toRunNodeId": cascadeUrls.listPreRunNodeByFlowInstId,
"runJob.toRunNodeId": cascadeUrls.listAnyJumpRunNodeByFlowInstId,
flowKey: cascadeUrls.getRunFlowByFlowInstId,
defFlowId: cascadeUrls.getRunFlowByFlowInstId,
userId: cascadeUrls.listUserByRoleId,
flowInstId: cascadeUrls.getRunFlowByCode,
runNodeId: cascadeUrls.listRunNodeByFlowInstId,
runJobId: cascadeUrls.listRunJobByRunNodeId,
distRunJobId: cascadeUrls.listDistRunJobByRunNodeId,
fromRunNodeId: cascadeUrls.listRunNodeByFlowInstId,
toRunNodeId: cascadeUrls.listRunNodeByFlowInstId,
handleUserId: cascadeUrls.listUsersByRunNodeId,
anyJumpUserId: cascadeUrls.listUserByRunNodeId,
anyJumpRoleId: cascadeUrls.listRoleByRunNodeId,
anyJumpPostId: cascadeUrls.listPostByRunNodeId,
anyJumpDeptId: cascadeUrls.listDeptByRunNodeId,
}
/**
* 表格级联数据事件
* dynkey 动态表格KEY
*/
export const onCascaded = (...keyObjs: DicDataInterface[]) => {
return async (state: any, form?: any, dynkey?: any) => {
let dataList = dynkey? form[dynkey] : state.dataList;
if (dataList && dataList.length > 0) {
try {
let cascadeDic = {}
for (const keyObj of keyObjs) {
let {key, dicUrl, cascades} = keyObj;
if (!cascades) continue;
for (const cascade of cascades) {
let realDicUrl = dicUrl;
if (!realDicUrl) realDicUrl = dicCascadeFun[cascade];
for (let i = 0; i < dataList.length; i++) {
let param = dataList[i][key];
if (validateNull(param)) continue;
if (!validateNull(cascadeDic[param])) continue
// @ts-ignore
let url = realDicUrl.replace("{{key}}", param)
if (url.indexOf("?") !== -1) {
let params = url.split('?')[1].split('=');
let p = dataList[i][params[0]];
if (validateNull(p)) continue;
url = url.replace("{{" + params[0] + "}}", p)
}
// @ts-ignore
let res = await listDicUrl(url);
if (validateNull(res.data)) continue;
cascadeDic[param] = res.data
}
}
}
Object.assign(state.cascadeDic, cascadeDic);
} catch (err: any) {
// 捕获异常并显示错误提示
ElMessage.error(err.msg || err.data.msg);
}
}
}
};
/**
* 表单级联数据变更事件
* index 动态表格index
*/
export const onCascadeChange = (cascadeDic: any, ...keyObjs: DicDataInterface[]) => {
return async (form: any, keyObjs2?: DicDataInterface, dynkey?: any, index?: number) => {
try {
let realKeyObjs = validateNull(keyObjs) ? {} : deepClone(keyObjs)
if (keyObjs2 && !validateNull(keyObjs2)) realKeyObjs = [deepClone(keyObjs2)]
for (const keyObj of realKeyObjs) {
let {key, dicUrl, cascades, prefix} = keyObj;
if (!cascades) continue;
for (const cascade of cascades) {
let realDicUrl = dicUrl;
let reqKey = cascade;
if (prefix) reqKey = prefix + "." + cascade;
if (!realDicUrl) realDicUrl = dicCascadeFun[reqKey];
// @ts-ignore
let param = dynkey ? form[dynkey][index][key] : form[key];
if (!realDicUrl || validateNull(param)) {
if (validateNull(param)) clearCascadeVal(form, keyObjs2, cascade, dynkey, index)
continue;
}
let url = realDicUrl.replace("{{key}}", param)
if (url.indexOf("?") !== -1) {
let params = url.split('?')[1].split('=');
// @ts-ignore
let p = dynkey ? form[dynkey][index][params[0]] : form[params[0]];
if (validateNull(p)) continue;
url = url.replace("{{" + params[0] + "}}", p)
}
// @ts-ignore
let res = await listDicUrl(url);
if (validateNull(res.data)) res.data = [];
let data = res.data;
if (!(data instanceof Array)) data = data ? [data] : []
if (typeof(index) !== 'undefined') {
cascadeDic[param] = data;
} else {
cascadeDic[cascade] = data;
}
clearCascadeVal(form, keyObjs2, cascade, dynkey, index)
}
}
} catch (err: any) {
// 捕获异常并显示错误提示
ElMessage.error(err.msg || err.data.msg);
}
}
};
function clearCascadeVal(form, keyObjs2, cascade, dynkey, index){
if (typeof(index) !== 'undefined') {
if (!validateNull(keyObjs2)) form[dynkey][index][cascade] = null
} else {
if (!validateNull(keyObjs2)) form[cascade] = null
}
}
const dicUrls = {
flowApplicationGroupName: "/order/flow-application/list/group-name",
listFlowApplication: "/order/flow-application/list",
defFlows: "/jsonflow/def-flow/list",
defFlowGroupName: "/jsonflow/def-flow/list/group-name",
listRoleName: "/jsonflow/user-role-auditor/list/roles?roleName={{roleName}}",
listTableName: "/order/create-table/list",
listUserKey: "/jsonflow/node-job/list/user-key",
listFlowNodeId: "/jsonflow/flow-node/list",
listNodeJobId: "/jsonflow/node-job/list",
runFlows: "/jsonflow/run-flow/list",
listDept: "/jsonflow/user-role-auditor/list/depts?deptName={{deptName}}",
listPost: "/jsonflow/user-role-auditor/list/posts?postName={{postName}}",
listUserKeyVal: "/jsonflow/node-job/list/user-key-val",
listVarKey: "/jsonflow/flow-node-rel/list/var-key",
listVarKeyVal: "/jsonflow/flow-node-rel/list/var-key-val",
listVarVal: "/jsonflow/flow-clazz/list/var-val",
listUsers: "/jsonflow/user-role-auditor/list/users?userName={{userName}}",
}
/**
* 默认回调映射关系
*/
const dicUrlFun = {
groupName: dicUrls.flowApplicationGroupName,
"defFlow.groupName": dicUrls.defFlowGroupName,
flowKey: dicUrls.defFlows,
carbonCopy: dicUrls.listUsers,
carbonCopyPerson: dicUrls.listUsers,
tableName: dicUrls.listTableName,
permission: dicUrls.listRoleName,
defFlowId: dicUrls.defFlows,
userKey: dicUrls.listUserKey,
roleId: dicUrls.listRoleName,
userId: dicUrls.listUsers,
handleUserId: dicUrls.listUsers,
flowNodeId: dicUrls.listFlowNodeId,
nodeJobId: dicUrls.listNodeJobId,
initiatorId: dicUrls.listUsers,
createUser: dicUrls.listUsers,
code: dicUrls.runFlows,
flowInstId: dicUrls.runFlows,
receiveUser: dicUrls.listUsers,
receiveDept: dicUrls.listDept,
defFlows: dicUrls.defFlows,
userKeys: dicUrls.listUserKey,
userKeyVals: dicUrls.listUserKeyVal,
varKeyVals: dicUrls.listVarKeyVal,
varVals: dicUrls.listVarVal,
roles: dicUrls.listRoleName,
roles2: dicUrls.listRoleName,
users: dicUrls.listUsers,
users2: dicUrls.listUsers,
delRoleId: dicUrls.listUsers,
posts: dicUrls.listPost,
posts2: dicUrls.listPost,
depts: dicUrls.listDept,
depts2: dicUrls.listDept,
formId: dicUrls.listFlowApplication,
}
/**
* 加载业务字典数据
*/
export const onLoadDicUrl = (...keyObjs: DicDataInterface[]) => {
return async (dicData: any, form?: any, keyObjs2?: DicDataInterface) => {
try {
let realKeyObjs = validateNull(keyObjs) ? {} : deepClone(keyObjs)
if (keyObjs2 && !validateNull(keyObjs2)) realKeyObjs = [deepClone(keyObjs2)]
if (!realKeyObjs) return
for (const keyObj of realKeyObjs) {
let {key, dicUrl, prefix} = keyObj;
let res;
if (dicUrl) res = await listDicUrl(dicUrl);
else {
let reqKey = key;
if (prefix) reqKey = prefix + "." + key;
let url = dicUrlFun[reqKey]
if (url.indexOf("?") !== -1) {
if (!validateNull(form)) {
let params = url.split('?')[1].split('=');
let param = form[params[0]];
if (validateNull(param)) continue;
url = url.replace("{{" + params[0] + "}}", param)
} else {
url = url.split('?')[0]
}
}
res = await listDicUrl(url);
}
validateNull(res.data) ? dicData[key] = [] : dicData[key] = res.data;
}
} catch (err: any) {
// 捕获异常并显示错误提示
ElMessage.error(err.msg || err.data.msg);
}
}
};
/**
* 更新业务字典数据
*/
export const onUpdateDicData = (...keyObjs: DicDataInterface[]) => {
return async (dicData: any, form: any, formKey?: any) => {
if (!validateNull(dicData) && !validateNull(form)) {
try {
for (const keyObj of keyObjs) {
let { key } = keyObj;
let newVal = form[key];
let dicDataVal = dicData[key];
if (validateNull(newVal) || validateNull(dicDataVal)) continue;
else {
let exist = dicDataVal.filter(f => f[key] === newVal);
if (!validateNull(exist)) continue;
}
let realKey = formKey ? formKey : key;
dicData[key].unshift({[realKey]: newVal});
}
} catch (err: any) {
// 捕获异常并显示错误提示
ElMessage.error(err.msg || err.data.msg);
}
}
}
};
export async function remoteMethodByKey(query, onLoad, dicData, param, key) {
if (query) {
let form = {}
form[param] = query
await onLoad(dicData, form, {key: key});
} else {
dicData[key] = []
}
}
export function initRemoteMethodAllByKey(onLoad, dicData) {
return {
async remoteMethodUser(query: string) {
await remoteMethodByKey(query, onLoad, dicData, 'userName', "users")
},
async remoteMethodRole(query: string) {
await remoteMethodByKey(query, onLoad, dicData, 'roleName', "roles")
},
async remoteMethodPost(query: string) {
await remoteMethodByKey(query, onLoad, dicData, 'postName', "posts")
},
async remoteMethodDept(query: string) {
await remoteMethodByKey(query, onLoad, dicData, 'deptName', "depts")
},
async remoteMethodUser2(query: string) {
await remoteMethodByKey(query, onLoad, dicData, 'userName', "users2")
},
async remoteMethodRole2(query: string) {
await remoteMethodByKey(query, onLoad, dicData, 'roleName', "roles2")
},
async remoteMethodPost2(query: string) {
await remoteMethodByKey(query, onLoad, dicData, 'postName', "posts2")
},
async remoteMethodDept2(query: string) {
await remoteMethodByKey(query, onLoad, dicData, 'deptName', "depts2")
}
}
}
export async function remoteMethodAllByKey(onLoad, dicData, query: string, jobType) {
if (jobType === DIC_PROP.JOB_USER_TYPE[0].value) {
await remoteMethodByKey(query, onLoad, dicData, 'userName', "users")
} else if (jobType === DIC_PROP.JOB_USER_TYPE[1].value) {
await remoteMethodByKey(query, onLoad, dicData, 'roleName', "roles")
} else if (jobType === DIC_PROP.JOB_USER_TYPE[2].value) {
await remoteMethodByKey(query, onLoad, dicData, 'postName', "posts")
} else if (jobType === DIC_PROP.JOB_USER_TYPE[3].value) {
await remoteMethodByKey(query, onLoad, dicData, 'deptName', "depts")
} else useMessage().warning("请先选择参与者类型")
}

View File

@@ -0,0 +1,74 @@
<template>
<div>
<template v-for="(item, index) in values">
<template v-if="validateGroupOptions(item)">
<span v-if="!props.elTagType" :key="index" :class="props.elTagClass" :style="props.style">
{{ showKeyName(item, values , index) }}</span>
<el-tag
v-else
:disable-transitions="true"
:key="index * 2"
:index="index"
:type="props.elTagType === 'primary' ? null : props.elTagType"
:class="props.elTagClass" :style="props.style"
>{{ showKeyName(item, values, index) }}</el-tag>
</template>
</template>
</div>
</template>
<script setup lang="ts" name="convert-group-name">
import { computed } from 'vue';
const props = defineProps({
// 数据
options: {
type: Array as any,
default: [],
},
// 当前的值
value: [Number, String, Array],
// 当前KEY
valueKey: String,
// 显示KEY
showKey: String,
// type
elTagType: String,
// class
elTagClass: String,
// style
style: [String, Object]
});
const values = computed(() => {
if (props.value !== null && typeof props.value !== 'undefined') {
return Array.isArray(props.value) ? props.value : [String(props.value)];
} else {
return [];
}
});
function validateGroupOptions(item){
let exist = null
for (const option of props.options) {
let find = option.options.find(f => f[props.valueKey] === item);
if (find) {
exist = find
break;
}
}
return exist
}
function showKeyName(item, values, index){
let exist = validateGroupOptions(item)
return exist[props.showKey] + (index !== (values.length - 1) ? "," : "")
}
</script>
<style scoped>
.el-tag + .el-tag {
margin-left: 10px;
}
</style>

View File

@@ -0,0 +1,62 @@
<template>
<div>
<template v-for="(item, index) in values">
<template v-if="props.options.some(s => s[props.valueKey] === item)">
<span v-if="!props.elTagType" :key="index" :class="props.elTagClass" :style="props.style">
{{ showKeyName(item, values , index) }}</span>
<el-tag
v-else
:disable-transitions="true"
:key="index * 2"
:index="index"
:type="props.elTagType === 'primary' ? null : props.elTagType"
:class="props.elTagClass" :style="props.style"
>{{ showKeyName(item, values, index) }}</el-tag>
</template>
</template>
</div>
</template>
<script setup lang="ts" name="convert-name">
import { computed } from 'vue';
const props = defineProps({
// 数据
options: {
type: Array as any,
default: [],
},
// 当前的值
value: [Number, String, Array],
// 当前KEY
valueKey: String,
// 显示KEY
showKey: String,
// type
elTagType: String,
// class
elTagClass: String,
// style
style: [String, Object]
});
const values = computed(() => {
if (props.value !== null && typeof props.value !== 'undefined') {
return Array.isArray(props.value) ? props.value : [String(props.value)];
} else {
return [];
}
});
function showKeyName(item, values, index){
let find = props.options.find(f => f[props.valueKey] === item);
return find[props.showKey] + (index !== (values.length - 1) ? "," : "")
}
</script>
<style scoped>
.el-tag + .el-tag {
margin-left: 10px;
}
</style>

View File

@@ -0,0 +1,156 @@
<template>
<div>
<el-dialog
v-if="data.showRoleUsers"
v-model="data.showRoleUsers"
top="20px" width="70%"
title="参与者候选人员"
append-to-body>
<el-table :data="data.dataList"
border
style="width: 100%">
<el-table-column :label="$t('sysuser.index')" type="index" width="40" />
<el-table-column :label="$t('sysuser.username')" prop="username" show-overflow-tooltip></el-table-column>
<el-table-column :label="$t('sysuser.name')" prop="name" show-overflow-tooltip></el-table-column>
<el-table-column :label="$t('sysuser.phone')" prop="phone" show-overflow-tooltip></el-table-column>
</el-table>
</el-dialog>
<template v-for="(item, index) in values">
<template v-if="!validateNull(props.options) && validateOptionsNotNull()">
<span v-if="!props.elTagType && !validateJobRoleType(item)" :key="index" :class="props.elTagClass">
{{ showKeyNameByJobType(item, values , index) }}</span>
<el-tooltip content="点击可查看参与者具体的人员信息" placement="top" v-if="!props.elTagType && validateJobRoleType(item)" :key="index">
<el-link type="primary" @click="handleShowJobRoleUsers(item)">
{{ showKeyNameByJobType(item, values , index) }}</el-link>
</el-tooltip>
<el-tag
v-if="props.elTagType"
:disable-transitions="true"
:key="index * 2"
:index="index"
:type="props.elTagType === 'primary' ? null : props.elTagType"
:class="props.elTagClass"
>{{ showKeyNameByJobType(item, values, index) }}</el-tag>
</template>
<template v-else-if="validateNull(props.options) && item.jobType">
<span v-if="!props.elTagType && !validateJobRoleType(item)" :key="index" :class="props.elTagClass">
{{ showRoleNameByJobType(item, values , index) }}</span>
<el-tooltip content="点击可查看参与者具体的人员信息" placement="top" v-if="!props.elTagType && validateJobRoleType(item)" :key="index">
<el-link type="primary" @click="handleShowJobRoleUsers(item)">
{{ showRoleNameByJobType(item, values , index) }}</el-link>
</el-tooltip>
<el-tag
v-if="props.elTagType"
:disable-transitions="true"
:key="index * 2"
:index="index"
:type="props.elTagType === 'primary' ? null : props.elTagType"
:class="props.elTagClass"
>{{ showRoleNameByJobType(item, values, index) }}</el-tag>
</template>
</template>
</div>
</template>
<script setup lang="ts" name="convert-role-name">
import { computed } from 'vue';
import {validateNull} from "/@/utils/validate";
import {DIC_PROP} from "/@/flow/support/dict-prop";
import {handleShowNameByJobType, handleShowRoleNameByJobType} from "/@/flow";
import {listUsersByRoleId} from "/@/api/jsonflow/common";
import {useI18n} from "vue-i18n";
const {t} = useI18n();
const props = defineProps({
// 数据
options: {
type: Object as any,
default: {},
},
// 当前的值
value: [Number, String, Object, Array],
// type
elTagType: String,
// class
elTagClass: String,
isJobType: String
});
const data = reactive({
showRoleUsers: false,
dataList: [],
});
const values = computed(() => {
if (props.value !== null && typeof props.value !== 'undefined') {
if (Array.isArray(props.value)) return props.value
else if (typeof props.value === 'object') {
return [props.value]
}
return [String(props.value)];
} else {
return [];
}
});
function validateOptionsNotNull(){
return props.options.users && props.options.roles && props.options.posts && props.options.depts
}
function showRoleNameByJobType(role, values, index){
if (role.roleName) {
let name = handleShowRoleNameByJobType(role, props.isJobType)
return name + (index !== (values.length - 1) ? "," : "")
} else {
showNameByJobType(role, values, index)
}
}
function showNameByJobType(role, values, index){
let name = handleShowNameByJobType(role, props.isJobType)
return name + (index !== (values.length - 1) ? "," : "")
}
function validateExist(role){
let exist;
if (role.jobType === DIC_PROP.JOB_USER_TYPE[0].value) {
exist = props.options.users.find(s => s.userId === role.roleId)
} else if (role.jobType === DIC_PROP.JOB_USER_TYPE[1].value) {
exist = props.options.roles.find(s => s.roleId === role.roleId)
} else if (role.jobType === DIC_PROP.JOB_USER_TYPE[2].value) {
exist = props.options.posts.find(s => s.postId === role.roleId)
} else if (role.jobType === DIC_PROP.JOB_USER_TYPE[3].value) {
exist = props.options.depts.find(s => s.deptId === role.roleId)
}
if (exist) exist.jobType = role.jobType
else exist = {}
return exist
}
function handleShowJobRoleUsers(item){
if (!item.roleId || !item.jobType) return
listUsersByRoleId(item.roleId, item.jobType).then(resp => {
data.dataList = resp.data
data.showRoleUsers = true
})
}
function validateJobRoleType(item){
let isRoleType = item.jobType !== DIC_PROP.JOB_USER_NONE_TYPE[0].value && item.jobType !== DIC_PROP.JOB_USER_NONE_TYPE[1].value;
return isRoleType && item.roleId
}
function showKeyNameByJobType(role, values, index){
let exist = validateExist(role);
return showNameByJobType(exist, values, index)
}
</script>
<style scoped>
.el-tag + .el-tag {
margin-left: 10px;
}
</style>

View File

@@ -0,0 +1,141 @@
<template>
<div class="layout-padding">
<div>
<!-- 动态组件 -->
<dynamic-link v-if="data.currElTab.active" :currJob="props.currJob" :currElTab="data.currElTab"></dynamic-link>
<template v-if="operType !== 'view'">
<div style="text-align: center">
<span class="dialog-footer">
<template v-if="props.currJob.status !== DIC_PROP.ORDER_STATUS[0].value">
<el-button type="primary" @click="methods.handleForm('onSubmit')" :disabled="loading">{{ $t('jfI18n.initialBtn') }}</el-button>
<el-button @click="methods.cancelButton">{{ $t('common.cancelButtonText') }}</el-button>
<el-button type="primary" @click="methods.handleForm('onTemp')" :disabled="loading">{{ $t('jfI18n.temp') }}</el-button>
</template>
<template v-else>
<el-button type="primary" @click="methods.handleForm('onSubmit')" :disabled="loading">{{ $t('common.editBtn') }}</el-button>
<el-button type="primary" @click="printForm" v-if="props.currJob.printInfo">{{
t('jfI18n.print')
}}
</el-button>
</template>
</span>
</div>
</template>
<template v-else>
<div style="text-align: center">
<span class="dialog-footer">
<el-button type="primary" @click="printForm" v-if="props.currJob.printInfo">{{
t('jfI18n.print')
}}
</el-button>
</span>
</div>
</template>
</div>
<!-- 打印表单 -->
<el-dialog v-model="data.showTinymceView" top="20px" width="700px"
:title="data.tinymceTitle" append-to-body
@close="closePrint">
<tinymce-view v-if="data.showTinymceView" :currFlowForm="props.currJob" :elTab="data.currElTab"></tinymce-view>
</el-dialog>
</div>
</template>
<script setup lang="ts" name="CustomForm">
import {useI18n} from 'vue-i18n';
import {useMessage} from "/@/hooks/message";
const {t} = useI18n();
// 引入组件
const DynamicLink = defineAsyncComponent(() => import('../handle-job/dynamic-link.vue'));
const TinymceView = defineAsyncComponent(() => import('/@/flow/components/tinymce/TinymceView.vue'));
const loading = ref(false);
const operType = ref(false);
const title = ref('');
const props = defineProps({
currJob: {
type: Object,
default: null,
}
});
const data = reactive({
currElTab: {active: null},
showTinymceView: false,
tinymceTitle: null,
});
const $emit = defineEmits(['onHandleForm']);
const methods = {
// 初始化数据
initJobData() {
methods.openDialog(props.currJob.operType)
data.currElTab = props.currJob.currElTab
data.currElTab.active = data.currElTab.id
},
// 打开弹窗
openDialog(type: string) {
operType.value = type;
if (type === 'add') {
title.value = t('common.addBtn');
} else if (type === 'edit') {
title.value = t('common.editBtn');
} else if (type === 'view') {
title.value = t('common.viewBtn');
} else if (type === 'copy') {
title.value = t('common.copyBtn');
}
},
// 提交表单
async handleForm(method) {
let exist = props.currJob[method]
if (exist) {
let res = await exist();
if (res === true) methods.cancelButton()
} else {
useMessage().info('正在加载, 请稍等')
}
},
cancelButton(){
$emit('onHandleForm', false)
}
}
function printForm() {
props.currJob.resolvePrintForm()
data.tinymceTitle = props.currJob.formName
data.showTinymceView = true
}
function closePrint(){
props.currJob.resolveClosePrint()
}
async function getFormData() {
return await props.currJob.getFormData()
}
// 暴露变量
defineExpose({
getFormData,
})
// 监听双向绑定
watch(
() => props.currJob.id,
(val) => {
methods.initJobData();
}
);
onMounted(() => {
methods.initJobData();
});
</script>

View File

@@ -0,0 +1,44 @@
import FcDesigner from 'form-create-designer';
import {listDicData, listDicUrl} from "/@/api/jsonflow/common";
import {validateNull} from "/@/utils/validate";
import request from '/@/utils/request';
export function initFcDesignerFetch(formRef, formData, globalData) {
// 配置表单请求拦截器
FcDesigner.designerForm.fetch = FcDesigner.formCreate.fetch = async (options: any) => {
// 发起请求
let res
if (options.method === 'GET') {
res = await listDicUrl(options.action, options.query);
} else {
if (options.file) {
res = await handleHttpUpload(options)
options.onSuccess(res);
return
} else {
res = await listDicData(options.action, options.data);
}
}
if (validateNull(res.data)) return
options.onSuccess(res.data);
};
}
const handleHttpUpload = async (options) => {
let formData = new FormData();
formData.append('file', options.file);
try {
return await request({
url: options.action,
method: 'post',
headers: {
'Content-Type': 'multipart/form-data',
'Enc-Flag': 'false',
},
data: formData,
});
} catch (error) {
options.onError(error as any);
}
};

View File

@@ -0,0 +1,167 @@
<template>
<div :class="!props.layoutPadding ? '' : 'layout-padding'">
<div :class="!props.layoutPadding ? '' : 'layout-padding-auto layout-padding-view'">
<fc-designer ref="designer" height="100vh" :config="config.designer" :handle="config.handle" @save="props.saveFormInfo"/>
</div>
</div>
</template>
<script setup lang="ts" name="FlowForm">
import FcDesigner from 'form-create-designer';
import type { Config, DragRule } from 'form-create-designer';
import ZhCn from 'form-create-designer/src/locale/zh-cn';
import En from 'form-create-designer/src/locale/en';
import {Local} from '/@/utils/storage';
import { rules } from './rules';
import {useI18n} from "vue-i18n"
import {validateNull} from "/@/utils/validate";
import {useUserInfo} from "/@/stores/userInfo";
import {deepClone} from "/@/utils/other";
import {notifyLeft} from "../../index";
import {initFcDesignerFetch} from "./api";
const {t} = useI18n();
const {proxy} = getCurrentInstance();
const props = defineProps({
currFlowForm: {
type: Object,
default: null,
},
layoutPadding: {
type: Boolean,
default: true,
},
showSaveBtn: {
type: Boolean,
default: false,
},
saveFormInfo: {
type: Function,
default: null,
},
});
// 组件引用
const designer = ref<InstanceType<typeof FcDesigner> | null>(null);
// 设计器配置
const config = {
designer: {
fieldReadonly: false, // 字段是否只读
showSaveBtn: props.showSaveBtn,
},
handle: [
{
label: '中英切换',
handle: () => {
changeLocale();
},
},
],
locale: null,
}
function changeLocale(isInit?) {
if (!isInit) {
if (config.locale === En) {
config.locale = ZhCn;
} else {
config.locale = En;
}
} else {
// 语言
let globalI18n = Local.get('themeConfig').globalI18n;
if (globalI18n === 'en') {
config.locale = En;
} else {
config.locale = ZhCn;
}
}
FcDesigner.useLocale(config.locale);
}
const methods = {
syncCurrFlowForm() {
// 保存表单
let json = designer.value.getRule()
if (validateNull(json)) props.currFlowForm.formInfo = null
else {
let options = designer.value.getOptions()
options.widgetList = json
props.currFlowForm.formInfo = options
}
},
handleSubmit(callback, isNotify) {
methods.syncCurrFlowForm()
let json = props.currFlowForm.formInfo
if (validateNull(json)) {
if (isNotify) notifyLeft('表单设计请添加组件', 'warning')
return false
}
props.currFlowForm.formDesign = true
if (callback) callback()
return true
}
}
const data = reactive({
globalDsv: {
type: Object,
default: {},
}
});
function initFcDesigner() {
designer.value.addMenu({ name: 'biz', title: '业务组件', list: []});
// Add all rules at once
rules.forEach((rule) => {
designer.value?.addComponent(rule as DragRule);
});
initFcDesignerFetch(designer.value, null, data.globalDsv)
changeLocale(true);
}
function getFormInfo() {
initFcDesigner()
data.globalDsv.userInfos = useUserInfo().userInfos
let formInfo = deepClone(props.currFlowForm.formInfo)
if (validateNull(formInfo)) {
designer.value.setRule([]);
return
}
designer.value.setRule(formInfo.widgetList);
delete formInfo.widgetList
designer.value.setOptions(formInfo);
}
onMounted(() => {
nextTick(() => {
getFormInfo();
})
});
// 暴露变量
defineExpose({
handleSubmit: methods.handleSubmit
});
</script>
<style lang="scss">
.fc-form-row {
// 选中效果
._fd-drag-tool.active {
outline: 2px solid #2e73ff !important;
}
// 栅格线条
._fd-drag-tool {
outline: 1px dashed var(--fc-tool-border-color)!important;
}
}
// 设置事件样式
._fd-event-l {
.el-menu-item {
line-height: 1em!important;
}
}
</style>

View File

@@ -0,0 +1,46 @@
<template>
<div style="width: 100%">
<el-input v-model="data.modelValue" @blur="changeModelValue" :disabled="props.disabled" clearable>
</el-input>
</div>
</template>
<script setup lang="ts" name="UserRolePickerIndex">
import {useMessage} from "/@/hooks/message";
import {verifyEmail} from "/@/utils/toolsValidate";
const props = defineProps({
modelValue: {
type: String,
default: null,
},
disabled: {
type: Boolean,
default: false,
},
});
const emits = defineEmits(['update:modelValue']);
const $message = useMessage();
const data = reactive({
modelValue: null
})
const changeModelValue = () => {
if (data.modelValue) {
let boolean = verifyEmail(data.modelValue);
if (!boolean) {
data.modelValue = null
$message.success("请输入正确的邮箱")
}
emits('update:modelValue', data.modelValue);
} else {
emits('update:modelValue', null);
}
};
onMounted(() => {
data.modelValue = props.modelValue
})
</script>

View File

@@ -0,0 +1,40 @@
<template>
<div style="width: 100%">
<el-input v-model="data.modelValue" @blur="changeModelValue" :disabled="props.disabled" clearable>
</el-input>
</div>
</template>
<script setup lang="ts" name="UserRolePickerIndex">
import {useMessage} from "/@/hooks/message";
const props = defineProps({
modelValue: {
type: String,
default: null,
},
disabled: {
type: Boolean,
default: false,
},
});
const emits = defineEmits(['update:modelValue']);
const $message = useMessage();
const data = reactive({
modelValue: null
})
const changeModelValue = () => {
if (data.modelValue) {
emits('update:modelValue', data.modelValue);
} else {
emits('update:modelValue', null);
}
};
onMounted(() => {
data.modelValue = props.modelValue
})
</script>

View File

@@ -0,0 +1,40 @@
<template>
<div style="width: 100%">
<el-input v-model="data.modelValue" @blur="changeModelValue" :disabled="props.disabled" clearable>
</el-input>
</div>
</template>
<script setup lang="ts" name="UserRolePickerIndex">
import {useMessage} from "/@/hooks/message";
const props = defineProps({
modelValue: {
type: String,
default: null,
},
disabled: {
type: Boolean,
default: false,
},
});
const emits = defineEmits(['update:modelValue']);
const $message = useMessage();
const data = reactive({
modelValue: null
})
const changeModelValue = () => {
if (data.modelValue) {
emits('update:modelValue', data.modelValue);
} else {
emits('update:modelValue', null);
}
};
onMounted(() => {
data.modelValue = props.modelValue
})
</script>

View File

@@ -0,0 +1,40 @@
<template>
<div style="width: 100%">
<el-input v-model="data.modelValue" @blur="changeModelValue" :disabled="props.disabled" clearable>
</el-input>
</div>
</template>
<script setup lang="ts" name="UserRolePickerIndex">
import {useMessage} from "/@/hooks/message";
const props = defineProps({
modelValue: {
type: String,
default: null,
},
disabled: {
type: Boolean,
default: false,
},
});
const emits = defineEmits(['update:modelValue']);
const $message = useMessage();
const data = reactive({
modelValue: null
})
const changeModelValue = () => {
if (data.modelValue) {
emits('update:modelValue', data.modelValue);
} else {
emits('update:modelValue', null);
}
};
onMounted(() => {
data.modelValue = props.modelValue
})
</script>

View File

@@ -0,0 +1,46 @@
<template>
<div style="width: 100%">
<el-input v-model="data.modelValue" @blur="changeModelValue" :disabled="props.disabled" clearable>
</el-input>
</div>
</template>
<script setup lang="ts" name="UserRolePickerIndex">
import {useMessage} from "/@/hooks/message";
import {verifyIdCard} from "/@/utils/toolsValidate";
const props = defineProps({
modelValue: {
type: String,
default: null,
},
disabled: {
type: Boolean,
default: false,
},
});
const emits = defineEmits(['update:modelValue']);
const $message = useMessage();
const data = reactive({
modelValue: null
})
const changeModelValue = () => {
if (data.modelValue) {
let boolean = verifyIdCard(data.modelValue);
if (!boolean) {
data.modelValue = null
$message.success("请输入正确的身份证号")
}
emits('update:modelValue', data.modelValue);
} else {
emits('update:modelValue', null);
}
};
onMounted(() => {
data.modelValue = props.modelValue
})
</script>

View File

@@ -0,0 +1,51 @@
import {validateNull} from "/@/utils/validate";
const PRINT_REPLACE = {
value: "value",
date: ["yyyy", "MM", "dd", "HH", "mm", "ss"]
}
// 自定义组件打印格式
const PRINT_FORMAT = [{props: [], propTypes: ["elImage", "SignInput"], tag: "<img src=\"value\" alt=\"\" />"}
,{props: [], propTypes: ["checkbox"], typeVals: [{value: "0", label: "□"},{value: "1", label: "☑"}]}
,{props: [], propTypes: ["radio"], typeVals: [{value: "0", label: "☑否 □是"},{value: "1", label: "□否 ☑是"}]}
,{props: [], propTypes: ["datePicker"], format: "yyyy_年_MM_月_dd_日_HH_时_mm_分_ss_秒"}]
export function handlePrintValue(optionItems, formDatum, value, label, prop?, propType?) {
if (!validateNull(optionItems)) {
if (Array.isArray(formDatum)) {
let showKeys = ''
for (let i = 0; i < formDatum.length; i++) {
let item = optionItems.find(f => f[value] === formDatum[i]);
if (item) {
if (i === formDatum.length -1) showKeys += handleFormatValue(item[value], item[label], prop, propType)
else showKeys += handleFormatValue(item[value], item[label], prop, propType) + ","
}
}
return showKeys
} else {
let item = optionItems.find(f => f[value] === formDatum);
if (item) return handleFormatValue(item[value], item[label], prop, propType)
}
}
}
export function handleFormatValue(value, label, prop?, propType?) {
if (validateNull(PRINT_FORMAT) || (!prop && !propType)) return label
let find = PRINT_FORMAT.find(f => f.props.includes(prop) || f.propTypes.includes(propType));
if (validateNull(find) || !value) return label
if (find.format) {
if (value instanceof Array) return label
let format = find.format
for (let i = 0; i < PRINT_REPLACE.date.length; i++) {
const eachFormat = PRINT_REPLACE.date[i];
let eachValue = ''
if (i <= 2) eachValue = value.split(" ")[0].split("-")[i]
else eachValue = value.split(" ")[1].split(":")[i - 3]
format = format.replace(eachFormat, eachValue)
}
return format;
}
if (find.typeVals) return find.typeVals.find(f => f.value === value).label
if (find.tag) return find.tag.replace(PRINT_REPLACE.value, value)
}

View File

@@ -0,0 +1,47 @@
<template>
<div style="width: 100%">
<el-input v-model="data.modelValue" @blur="changeModelValue" :disabled="props.disabled" clearable>
</el-input>
</div>
</template>
<script setup lang="ts" name="UserRolePickerIndex">
import {rule} from "/@/utils/validate";
import {useMessage} from "/@/hooks/message";
const props = defineProps({
modelValue: {
type: String,
default: null,
},
disabled: {
type: Boolean,
default: false,
},
});
const emits = defineEmits(['update:modelValue']);
const $message = useMessage();
const data = reactive({
modelValue: null
})
const changeModelValue = () => {
if (data.modelValue) {
rule.validatePhone(null, data.modelValue, (err) => {
if (err) {
data.modelValue = null
$message.success(err.message)
}
emits('update:modelValue', data.modelValue);
})
} else {
emits('update:modelValue', null);
}
};
onMounted(() => {
data.modelValue = props.modelValue
})
</script>

View File

@@ -0,0 +1,93 @@
<template>
<div>
<form-create ref="formCreateRef" v-model="design.formData" v-model:api="design.fApi" :rule="design.rule"
:option="design.option">
</form-create>
</div>
</template>
<script setup lang="ts" name="FormRender">
import {useUserInfo} from "/@/stores/userInfo";
import {initFcDesignerFetch} from "./api";
import {validateNull} from "/@/utils/validate";
import {parseWithFunctions} from "../../index";
import {compatibleAppHeight} from "/@/api/order/order-key-vue";
import {validateApp} from "/@/views/order";
const formCreateRef = ref(null)
const props = defineProps({
currFlowForm: {
type: Object,
default: {},
},
initFormPermPrint: null,
renderType: null,
});
const design = reactive({
rule: [],
option: {},
fApi: null,
formData: null
});
const data = reactive({
globalDsv: {
type: Object,
default: {},
}
});
function doInitFcDesigner(formInfo) {
design.rule = formInfo.widgetList
delete formInfo.widgetList
design.option = formInfo
design.formData = parseWithFunctions(props.currFlowForm.formData)
// 隐藏提交按钮
design.option.submitBtn = false
}
const $route = useRoute();
async function initFcDesigner() {
if (validateNull(props.currFlowForm)) return
data.globalDsv.userInfos = useUserInfo().userInfos
let formInfoStr = props.currFlowForm.formInfo;
if (validateNull(formInfoStr)) return
initFcDesignerFetch(formCreateRef.value, design.formData, data.globalDsv)
let formInfo = parseWithFunctions(formInfoStr, true)
let elTab = null
// 保证组件事件后执行
if (props.initFormPermPrint) elTab = await props.initFormPermPrint(formInfo);
// 保证子表单新增行的字段权限生效
doInitFcDesigner(formInfo)
nextTick(async () => {
// 在nextTick中保证生效
if (elTab && elTab.isFormEdit === '0') design.fApi.disabled(true)
// 查看时在表单权限后disabled
if (props.renderType === '-1') design.fApi.disabled(true)
compatibleAppHeight(validateApp($route))
})
}
// 监听双向绑定
watch(
() => props.currFlowForm.id,
() => {
initFcDesigner();
}
);
onMounted(() => {
initFcDesigner()
})
// 暴露变量
defineExpose({
design
})
</script>
<style scoped lang="scss">
</style>

View File

@@ -0,0 +1,397 @@
import type { DragRule } from 'form-create-designer';
import other from "/@/utils/other";
// Create a map of rule creators
const ruleCreators: Record<string, () => DragRule> = {
UserPicker: () => ({
menu: 'biz',
icon: 'iconfont icon-icon-',
label: '人员',
name: 'UserPicker',
mask: true,
rule() {
return {
type: 'UserPicker',
field: 'UserPicker' + other.getNonDuplicateID(),
title: '人员',
$required: true,
props: {
multiple: false,
},
};
},
props() {
return [
{
type: 'switch',
title: '多选',
field: 'multiple',
value: false,
},
{
type: 'switch',
title: '禁用',
field: 'disabled',
value: false,
},
];
},
}),
RolePicker: () => ({
menu: 'biz',
icon: 'iconfont icon-gerenzhongxin',
label: '角色',
name: 'RolePicker',
mask: true,
rule() {
return {
type: 'RolePicker',
field: 'RolePicker' + other.getNonDuplicateID(),
title: '角色',
$required: true,
props: {
multiple: false,
},
};
},
props() {
return [
{
type: 'switch',
title: '多选',
field: 'multiple',
value: false,
},
{
type: 'switch',
title: '禁用',
field: 'disabled',
value: false,
}
];
},
}),
PostPicker: () => ({
menu: 'biz',
icon: 'iconfont icon-siweidaotu font14',
label: '岗位',
name: 'PostPicker',
mask: true,
rule() {
return {
type: 'PostPicker',
field: 'PostPicker' + other.getNonDuplicateID(),
title: '岗位',
$required: true,
props: {
multiple: false,
},
};
},
props() {
return [
{
type: 'switch',
title: '多选',
field: 'multiple',
value: false,
},
{
type: 'switch',
title: '禁用',
field: 'disabled',
value: false,
}
];
},
}),
DeptPicker: () => ({
menu: 'biz',
icon: 'iconfont icon-shouye',
label: '部门',
name: 'DeptPicker',
mask: true,
rule() {
return {
type: 'DeptPicker',
field: 'DeptPicker' + other.getNonDuplicateID(),
title: '部门',
$required: true,
props: {
multiple: false,
},
};
},
props() {
return [
{
type: 'switch',
title: '多选',
field: 'multiple',
value: false,
},
{
type: 'switch',
title: '禁用',
field: 'disabled',
value: false,
}
];
},
}),
PhoneInput: () => ({
menu: 'biz',
icon: 'iconfont icon-dianhua',
label: '手机号',
name: 'PhoneInput',
mask: true,
rule() {
return {
type: 'PhoneInput',
field: 'PhoneInput' + other.getNonDuplicateID(),
title: '手机号',
$required: true,
props: {
disabled: false,
},
};
},
props() {
return [
{
type: 'switch',
title: '禁用',
field: 'disabled',
value: false,
}
];
},
}),
IdCartInput: () => ({
menu: 'biz',
icon: 'iconfont icon-tupianyulan',
label: '身份证号',
name: 'IdCartInput',
mask: true,
rule() {
return {
type: 'IdCartInput',
field: 'IdCartInput' + other.getNonDuplicateID(),
title: '身份证号',
$required: true,
props: {
disabled: false,
},
};
},
props() {
return [
{
type: 'switch',
title: '禁用',
field: 'disabled',
value: false,
}
];
},
}),
FormNameInput: () => ({
menu: 'biz',
icon: 'iconfont icon-putong',
label: '表单名称',
name: 'FormNameInput',
mask: true,
rule() {
return {
type: 'FormNameInput',
field: 'formName',
title: '表单名称',
$required: true,
props: {
disabled: false,
},
};
},
props() {
return [
{
type: 'switch',
title: '禁用',
field: 'disabled',
value: false,
}
];
},
}),
FlowNameInput: () => ({
menu: 'biz',
icon: 'iconfont icon-shuxingtu',
label: '流程名称',
name: 'FlowNameInput',
mask: true,
rule() {
return {
type: 'FlowNameInput',
field: 'flowName',
title: '流程名称',
$required: true,
props: {
disabled: false,
},
};
},
props() {
return [
{
type: 'switch',
title: '禁用',
field: 'disabled',
value: false,
}
];
},
}),
FormCodeInput: () => ({
menu: 'biz',
icon: 'iconfont icon-quanjushezhi_o',
label: '流程CODE',
name: 'FormCodeInput',
mask: true,
rule() {
return {
type: 'FormCodeInput',
field: 'code',
title: '流程CODE',
$required: true,
props: {
disabled: false,
},
};
},
props() {
return [
{
type: 'switch',
title: '禁用',
field: 'disabled',
value: false,
}
];
},
}),
EmailInput: () => ({
menu: 'biz',
icon: 'iconfont icon-xingqiu',
label: '邮箱',
name: 'EmailInput',
mask: true,
rule() {
return {
type: 'EmailInput',
field: 'EmailInput' + other.getNonDuplicateID(),
title: '邮箱',
$required: true,
props: {
disabled: false,
},
};
},
props() {
return [
{
type: 'switch',
title: '禁用',
field: 'disabled',
value: false,
}
];
},
}),
SignInput: () => ({
menu: 'biz',
icon: 'icon-edit',
label: '签名',
name: 'SignInput',
mask: true,
rule() {
return {
type: 'SignInput',
field: 'SignInput'+ other.getNonDuplicateID(),
title: '签名',
$required: true,
props: {
bgColor: '#F6F8FA',
isClearBgColor: false,
disabled: false,
},
};
},
props() {
return [
{
type: 'switch',
title: '禁用',
field: 'disabled',
value: false,
},
{
type: 'number',
title: '宽度',
field: 'width',
value: 300,
props: {
min: 100,
max: 1000,
},
},
{
type: 'number',
title: '高度',
field: 'height',
value: 150,
props: {
min: 50,
max: 500,
},
},
{
type: 'number',
title: '线宽',
field: 'lineWidth',
value: 4,
props: {
min: 1,
max: 20,
},
},
{
type: 'colorPicker',
title: '线条颜色',
field: 'lineColor',
value: '#000000',
},
{
type: 'colorPicker',
title: '背景颜色',
field: 'bgColor',
value: '#F6F8FA',
},
{
type: 'switch',
title: '裁剪空白',
field: 'isCrop',
value: false,
},
{
type: 'switch',
title: '清除背景',
field: 'isClearBgColor',
value: false,
},
];
},
}),
};
// Export all rules as an array
export const rules: any[] = Object.values(ruleCreators).map((creator) => creator());

View File

@@ -0,0 +1,176 @@
<template>
<div>
<el-form ref="dataFormRef" :model="form" :rules="dataRules" label-width="150px" v-loading="loading" :disabled="operType === 'view'">
<span>什么是抄送任务被抄送的参与者需要参与审批而传阅任务不需要参与审批只参与阅览</span>
<el-row :gutter="24">
<el-divider> 参与者设置 </el-divider>
<el-col :span="12" class="mb20">
<el-form-item label="参与者类型" prop="jobType">
<el-radio-group @change="handleRoleType"
v-model="form.jobType">
<el-radio v-for="(item, index) in DIC_PROP.JOB_USER_TYPE" :key="index" :label="item.value">
{{ item.label }}
</el-radio>
</el-radio-group>
</el-form-item>
</el-col>
<el-col :span="12" class="mb20" v-if="form.jobType === DIC_PROP.JOB_USER_TYPE[0].value">
<el-form-item label="指定参与人员" prop="roleId">
<el-select v-model="form.roleId" placeholder="请输入用户名称进行模糊搜索" clearable filterable
remote :remote-method="methodsRemote.remoteMethodUser" :reserve-keyword="false">
<el-option v-for="(item, index) in dicData.users" :key="index" :label="item.name" :value="item.userId"></el-option>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12" class="mb20" v-if="form.jobType === DIC_PROP.JOB_USER_TYPE[1].value">
<el-form-item label="指定参与角色" prop="roleId">
<el-select v-model="form.roleId" placeholder="请输入角色名称进行模糊搜索" clearable filterable
remote :remote-method="methodsRemote.remoteMethodRole" :reserve-keyword="false">
<el-option v-for="(item, index) in dicData.roles" :key="index" :label="item.roleName" :value="item.roleId"></el-option>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12" class="mb20" v-if="form.jobType === DIC_PROP.JOB_USER_TYPE[2].value">
<el-form-item label="指定参与岗位" prop="roleId">
<el-select v-model="form.roleId" placeholder="请输入岗位名称进行模糊搜索" clearable filterable
remote :remote-method="methodsRemote.remoteMethodPost" :reserve-keyword="false">
<el-option v-for="(item, index) in dicData.posts" :key="index" :label="item.postName" :value="item.postId"></el-option>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12" class="mb20" v-if="form.jobType === DIC_PROP.JOB_USER_TYPE[3].value">
<el-form-item label="指定参与部门" prop="roleId">
<el-select v-model="form.roleId" placeholder="请输入部门名称进行模糊搜索" clearable filterable
remote :remote-method="methodsRemote.remoteMethodDept" :reserve-keyword="false">
<el-option v-for="(item, index) in dicData.depts" :key="index" :label="item.name" :value="item.deptId"></el-option>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12" class="mb20">
<el-form-item :label="t('runJob.jobName')" prop="jobName">
<el-input v-model="form.jobName" :placeholder="t('runJob.inputJobNameTip')"/>
</el-form-item>
</el-col>
</el-row>
</el-form>
<footer class="el-dialog__footer">
<span class="dialog-footer">
<el-button type="primary" @click="handleUpdate" :loading="loading">{{ $t('common.confirmButtonText') }}</el-button>
</span>
</footer>
</div>
</template>
<script setup lang="ts" name="CopyPassJob">
import { useI18n } from "vue-i18n"
import {onLoadDicUrl, remoteMethodByKey} from "../convert-name/convert";
import {useMessage} from "/@/hooks/message";
import {handleChangeJobType} from "../../index";
const { t } = useI18n();
const $message = useMessage();
const $emit = defineEmits(['onJobSignature']);
// 定义变量内容
const dataFormRef = ref();
const loading = ref(false);
const operType = ref(false);
// 定义字典
const dicData = reactive({});
const cascadeDic = reactive({});
const onLoad = onLoadDicUrl();
onMounted(() => {
// onLoad(dicData);
});
// 提交表单数据
const form = reactive({
jobType: '',
roleId: null,
});
// 定义校验规则
const dataRules = ref({
jobType: [{required: true, message: '参与者类型不能为空', trigger: 'blur'}],
roleId: [{required: true, message: '任务参与者不能为空', trigger: 'blur'}],
})
const props = defineProps({
currJob: {
default: null,
}
});
function initJobData() {
Object.assign(form, props.currJob)
form.roleId = null
}
function handleRoleType() {
handleChangeJobType(dicData, form)
}
const methodsRemote = {
remoteMethodUser(query: string) {
remoteMethodByKey(query, onLoad, dicData, 'userName', "users")
},
remoteMethodRole(query: string) {
remoteMethodByKey(query, onLoad, dicData, 'roleName', "roles")
},
remoteMethodPost(query: string) {
remoteMethodByKey(query, onLoad, dicData, 'postName', "posts")
},
remoteMethodDept(query: string) {
remoteMethodByKey(query, onLoad, dicData, 'deptName', "depts")
},
}
async function handleUpdate() {
const valid = await dataFormRef.value.validate().catch(() => {});
if (!valid) return false;
form.nextUserRole = {jobType: form.jobType, roleId: form.roleId, jobName: form.jobName}
props.currJob.nextUserRole = form.nextUserRole
loading.value = true
try {
$emit('onJobSignature', props.currJob)
setTimeout(() => { // 异步异常
loading.value = false
}, 3000)
} catch (e) {
loading.value = false
}
}
// 监听双向绑定
watch(
() => props.currJob.id,
(val) => {
initJobData();
}
);
onMounted(() => {
initJobData();
});
</script>
<style lang="scss" scoped>
.el-dialog__footer {
text-align: center;
.dialog-footer {
text-align: center;
}
}
</style>

View File

@@ -0,0 +1,58 @@
<template>
<div class="layout-padding">
<el-scrollbar class="main">
<iframe :src="src" class="iframe" />
</el-scrollbar>
</div>
</template>
<script lang="ts" name="DynamicIframe" setup>
import { Session } from '/@/utils/storage';
const { proxy } = getCurrentInstance();
const route = useRoute();
const src = ref('');
const emits = defineEmits(["handleJob"]);
const props = defineProps({
currJob: {
type: Object,
default: null,
},
currElTab: {
type: Object,
default: null,
}
});
const init = () => {
const token = Session.getToken();
const tenantId = Session.getTenant();
let flowInstId = props.currJob.flowInstId;
let runJobId = props.currJob.id;
src.value = props.currElTab.path + `?token=${token}&tenantId=${tenantId}&flowInstId=${flowInstId}&runJobId=${runJobId}`;
};
function handleJob(jobBtn) {
emits("handleJob", jobBtn);
}
watch([route], () => {
init();
});
onMounted(() => {
init();
});
</script>
<style lang="scss" scoped>
.iframe {
width: 100%;
height: 80vh;
border: 0;
overflow: hidden;
box-sizing: border-box;
}
</style>

View File

@@ -0,0 +1,62 @@
<template>
<keep-alive v-if="data.currTabComp">
<component v-if="data.currTabComp" :key="props.currElTab.path" :is="data.currTabComp" :currJob="props.currJob" :currElTab="props.currElTab"
@handleJob="handleJob"></component>
</keep-alive>
</template>
<script setup lang="ts" name="DynamicLink">
import {deepClone} from "/@/utils/other";
import {useMessage} from "/@/hooks/message";
const emits = defineEmits(["handleJob"]);
const props = defineProps({
currJob: {
type: Object,
default: null,
},
currElTab: {
type: Object,
default: null,
}
});
const data = reactive({
currTabComp: null,
preElTab: null
});
function handLoader() {
let path = props.currElTab.path;
if (!path) {
data.currTabComp = null
useMessage().error("不存在的表单,请检查")
return
}
data.currTabComp = `../../views${path}.vue`
}
function handleJob(jobBtn) {
emits("handleJob", jobBtn);
}
// 监听双向绑定
watch(
() => props.currElTab.active,
(val) => {
let b = props.currElTab.active !== data.preElTab.active && props.currElTab.path === data.preElTab.path;
if (b) {
data.preElTab = deepClone(props.currElTab)
data.currTabComp = null
}
handLoader()// 重载
}
);
onMounted(() => {
data.preElTab = deepClone(props.currElTab)
// 初始化
if (props.currElTab.path) handLoader()
});
</script>

View File

@@ -0,0 +1,572 @@
<template>
<div class="layout-container">
<div class="layout-padding w100">
<div class="layout-padding-auto layout-padding-view">
<!-- <div style="margin-bottom: 10px;">
<div style="font-size: 20px;">{{ data.currJob.flowName }}</div>
<div style="font-weight: 300;">
<span >发起人{{ data.currJob.initiatorName }}</span>
<span style="margin-left: 10px">发起时间{{ data.currJob.initiatorTime }}</span>
</div>
</div>-->
<!-- 选项卡tabs -->
<el-tabs ref="tabs" v-model="data.currElTab.active" @tab-click="methods.handleClick">
<el-tab-pane v-for="(item, index) in data.elTabs"
:key="index"
:label="item.formName"
:name="item.id">
<template #label>
<div>
<i :class="item.icon" style="vertical-align: middle;margin-right: 3px"></i>
<span style="vertical-align: middle;">{{ item.formName }}</span>
</div>
</template>
</el-tab-pane>
</el-tabs>
<div id="handle_job">
<!-- 动态组件 -->
<template v-if="data.currElTab.path && data.currElTab.path.startsWith('http')">
<dynamic-iframe v-if="data.currElTab.active" :currJob="data.currJob" :currElTab="data.currElTab" @handleJob="methods.handleJob"></dynamic-iframe>
</template>
<template v-else>
<dynamic-link v-if="data.currElTab.active" :currJob="data.currJob" :currElTab="data.currElTab" @handleJob="methods.handleJob"></dynamic-link>
</template>
<!-- 审批项 -->
<el-form v-if="!data.currJob.hiJob && !orderVue.vueKeySys.sysPaths.includes(data.currElTab.path)" label-width="72px" style="margin-top: 22px;">
<template v-if="data.currElTab.isAutoAudit !== '1'">
<el-form-item :label="t('jfcomment.remark')">
<el-input v-model="data.currJob.comment" :autosize="{ minRows: 3, maxRows: 6}" maxlength="50" show-word-limit
type="textarea" :placeholder="t('jfcomment.inputRemarkTip')">
</el-input>
</el-form-item>
<el-form-item :label="t('jfcomment.signName')">
<sign-name ref="signNameRef" :currJob="data.currJob"></sign-name>
</el-form-item>
<el-form-item>
<template v-for="(jobBtn, index) in data.jobBtns.length > 6 ? data.jobBtns.slice(0, 6) : data.jobBtns" :key="index">
<el-button :loading="data.loading"
v-if="/*methods.handleJobBtns(jobBtn) */ true"
icon="Check"
plain
@click="methods.handleJob(jobBtn, true)">{{ methods.getJobBtnName(jobBtn) }}
</el-button>
</template>
<el-dropdown max-height="500px" size="small" style="vertical-align: middle; margin-left: 10px" placement="top" v-if="data.jobBtns.length > 6">
<el-button icon="More">{{ t('jfI18n.more') }}</el-button>
<template #dropdown>
<el-dropdown-menu v-if="/*methods.handleJobBtns(jobBtn) */ true">
<template v-for="(jobBtn, index) in data.jobBtns.slice(6)" :key="index">
<el-dropdown-item @click.native="methods.handleJob(jobBtn, true)" divided>
{{ methods.getJobBtnName(jobBtn) }}
</el-dropdown-item>
</template>
</el-dropdown-menu>
</template>
</el-dropdown>
</el-form-item>
</template>
</el-form>
</div>
<el-dialog
v-if="data.showToRunNode"
v-model="data.showToRunNode"
top="20px"
width="70%"
title="选择任意驳回节点"
append-to-body>
<run-reject :curr-job="data.currJob" @onRejectJob="btnMethods.onRejectJob"></run-reject>
</el-dialog>
<el-dialog
v-if="data.showJumpToRunNode"
v-model="data.showJumpToRunNode"
top="20px"
width="70%"
title="选择任意跳转节点"
append-to-body>
<run-any-jump :curr-job="data.currJob" @onAnyJumpJob="btnMethods.onAnyJumpJob"></run-any-jump>
</el-dialog>
<el-dialog
v-if="data.showJobSignature"
v-model="data.showJobSignature"
top="20px"
width="70%"
:title="data.jobSignatureTitle"
append-to-body>
<job-signature :curr-job="data.currJob" @onJobSignature="btnMethods.onJobSignature"></job-signature>
</el-dialog>
<el-dialog
v-if="data.showJobCopyPass"
v-model="data.showJobCopyPass"
top="20px"
width="70%"
:title="data.jobSignatureTitle"
append-to-body>
<copy-pass-job :curr-job="data.currJob" @onJobSignature="btnMethods.onJobSignature"></copy-pass-job>
</el-dialog>
<el-dialog
v-if="data.showNodeSignature"
v-model="data.showNodeSignature"
top="20px"
width="70%"
:title="data.nodeSignatureTitle"
append-to-body>
<node-signature :curr-job="data.currJob" @onNodeSignature="btnMethods.onNodeSignature"></node-signature>
</el-dialog>
<el-dialog
v-if="data.showDistPerson"
v-model="data.showDistPerson"
top="20px"
width="70%"
title="分配参与者"
append-to-body>
<dist-person :curr-job="data.currJob"></dist-person>
</el-dialog>
<user-role-picker ref="userRolePicker" :isOnlyOne="true" @onSelectItems="btnMethods.onSelectItems">
</user-role-picker>
</div>
</div>
</div>
</template>
<script setup lang="ts" name="HandleJob">
import {useI18n} from 'vue-i18n';
import {deepClone} from "/@/utils/other";
import {useMessage, useMessageBox} from "/@/hooks/message";
import {useUserInfo} from "/@/stores/userInfo";
import {TabsPaneContext} from "element-plus";
import * as orderVue from "/@/api/order/order-key-vue";
import {validateNull} from "/@/utils/validate";
import {DIC_PROP} from "../../support/dict-prop";
import {getJobBtnName} from "../../index";
import * as doJob from "/@/api/jsonflow/do-job";
import * as runNode from "/@/api/jsonflow/run-node";
import * as runFlow from "/@/api/jsonflow/run-flow";
import mittBus from '/@/utils/mitt';
import {useFlowJob} from "../../stores/flowJob";
import {setPropsDataValue} from "../../support/common";
import {initHandleJobHeight} from "../../utils";
import {handleTodoDetail, handleToDoneDetail} from "../../support/extend";
const {t} = useI18n();
const userInfo = useUserInfo();
const $message = useMessage();
const flowJob = useFlowJob();
const route = useRoute();
const {proxy} = getCurrentInstance();
// 引入组件
const DynamicLink = defineAsyncComponent(() => import('./dynamic-link.vue'));
const DynamicIframe = defineAsyncComponent(() => import('./dynamic-iframe.vue'));
const SignName = defineAsyncComponent(() => import('../sign-name/index.vue'));
const UserRolePicker = defineAsyncComponent(() => import('../user-role/picker.vue'));
const JobSignature = defineAsyncComponent(() => import('./job-signature.vue'));
const CopyPassJob = defineAsyncComponent(() => import('./copy-pass-job.vue'));
const NodeSignature = defineAsyncComponent(() => import('./node-signature.vue'));
const RunReject = defineAsyncComponent(() => import('/@/views/jsonflow/run-reject/flow.vue'));
const RunAnyJump = defineAsyncComponent(() => import('/@/views/jsonflow/run-job/flow.vue'));
const DistPerson = defineAsyncComponent(() => import('/@/views/jsonflow/dist-person/flow.vue'));
const props = defineProps({
currJob: {
type: Object,
default: {},
}
});
const data = reactive({
currJob: {},
// 0、转办 1、下一办理人
nodeUserType: undefined,
jobSignatureTitle: undefined,
showJobSignature: false,
showJobCopyPass: false,
nodeSignatureTitle: undefined,
showNodeSignature: false,
showJumpToRunNode: false,
showToRunNode: false,
showDistPerson: false,
elTabs: {},
jobBtns: [],
currElTab: {active: null},
loading: false,
});
const channel = new BroadcastChannel('currJob_channel');
const btnMethods = {
onHandleJob(jobBtn) {
data.currJob.jobBtn = jobBtn
let nodeSignatures = [DIC_PROP.JOB_BTNS[1].value, DIC_PROP.JOB_BTNS[2].value, DIC_PROP.JOB_BTNS[3].value];
if (jobBtn === DIC_PROP.JOB_BTNS[13].value) btnMethods.openJobRoleUserId('1')
else if (jobBtn === DIC_PROP.JOB_BTNS[14].value) btnMethods.openJobRoleUserId('0');
else if (jobBtn === DIC_PROP.JOB_BTNS[15].value) btnMethods.handleTerminateFlow(data.currJob);
else if (jobBtn === DIC_PROP.JOB_BTNS[16].value) btnMethods.handleEarlyComplete(data.currJob);
else if (jobBtn === DIC_PROP.JOB_BTNS[17].value) btnMethods.handleInvalidFlow(data.currJob);
else if (jobBtn === DIC_PROP.JOB_BTNS[18].value) btnMethods.doDistPerson();
else if (jobBtn === DIC_PROP.JOB_BTNS[11].value) btnMethods.doJobSignature(jobBtn)
else if (jobBtn === DIC_PROP.JOB_BTNS[12].value) btnMethods.doJobSignature(jobBtn)
else if (jobBtn === DIC_PROP.JOB_BTNS[7].value) btnMethods.onBackFirstJob(data.currJob)
else if (jobBtn === DIC_PROP.JOB_BTNS[8].value) btnMethods.onBackPreJob(data.currJob)
else if (nodeSignatures.includes(jobBtn)) btnMethods.doNodeSignature(jobBtn)
else btnMethods.doHandleJob(jobBtn)
},
doHandleJob(jobBtn) {
data.currJob.jobBtn = jobBtn
if (jobBtn === DIC_PROP.JOB_BTNS[9].value) {
return btnMethods.doAnyJumpJob()
}
if (jobBtn === DIC_PROP.JOB_BTNS[10].value) {
return btnMethods.doRejectJob()
}
let jobSignatures = [DIC_PROP.JOB_BTNS[4].value, DIC_PROP.JOB_BTNS[5].value, DIC_PROP.JOB_BTNS[6].value];
if (jobSignatures.includes(jobBtn)) {
return btnMethods.doJobSignature(jobBtn)
}
doJob.complete(data.currJob).then(() => {
$message.success('审批成功')
btnMethods.onContextmenuClose()
})
},
onBackFirstJob(currJob) {
doJob.backFirstJob(currJob).then(() => {
$message.success('退回首节点成功')
btnMethods.onContextmenuClose()
})
},
onBackPreJob(currJob) {
doJob.backPreJob(currJob).then(() => {
$message.success('退回上一步成功')
btnMethods.onContextmenuClose()
})
},
doAnyJumpJob() {
data.currJob.runRejectVO = {}
data.showJumpToRunNode = true
},
onAnyJumpJob(currJob) {
doJob.anyJump(currJob).then(() => {
$message.success('跳转成功')
data.showJumpToRunNode = false
btnMethods.onContextmenuClose()
})
},
doRejectJob() {
data.currJob.runRejectVO = {}
data.showToRunNode = true
},
onRejectJob(currJob) {
doJob.reject(currJob).then(() => {
$message.success('驳回成功')
data.showToRunNode = false
btnMethods.onContextmenuClose()
})
},
doDistPerson() {
data.showDistPerson = true
},
doJobSignature(jobBtn) {
data.currJob.nodeJobSigned = {}
data.jobSignatureTitle = getJobBtnName(jobBtn);
if (jobBtn === DIC_PROP.JOB_BTNS[11].value || jobBtn === DIC_PROP.JOB_BTNS[12].value) {
data.currJob.nodeJobSigned.signedType = DIC_PROP.SIGNATURE_TYPE[2].value
if (jobBtn === DIC_PROP.JOB_BTNS[11].value) data.currJob.nodeJobSigned.belongType = '1'
else data.currJob.nodeJobSigned.belongType = '2'
data.showJobCopyPass = true
} else {
let type = DIC_PROP.SIGNATURE_TYPE[0].value
if (DIC_PROP.JOB_BTNS[5].value === jobBtn) type = DIC_PROP.SIGNATURE_TYPE[1].value
else if (DIC_PROP.JOB_BTNS[6].value === jobBtn) type = DIC_PROP.SIGNATURE_TYPE[2].value
data.currJob.nodeJobSigned.signedType = type
data.currJob.nodeJobSigned.belongType = '0'
data.showJobSignature = true
}
},
doNodeSignature(jobBtn) {
data.currJob.nodeJobSigned = {}
data.nodeSignatureTitle = getJobBtnName(jobBtn);
data.currJob.nodeJobSigned.signedType = DIC_PROP.NODE_SEQUENTIAL_TYPE[2].value
let nodeType = DIC_PROP.NODE_TYPE[1].value
let type = DIC_PROP.NODE_SIGNATURE_TYPE[0].value
if (DIC_PROP.JOB_BTNS[2].value === jobBtn) type = DIC_PROP.NODE_SIGNATURE_TYPE[1].value
else if (DIC_PROP.JOB_BTNS[3].value === jobBtn) {
type = DIC_PROP.NODE_SIGNATURE_TYPE[2].value
nodeType = DIC_PROP.NODE_TYPE[2].value
if (data.currJob.runNodeVO.nodeType !== nodeType) {
$message.info('当前节点不是并行节点,必须在并行节点上才能加并节点')
return
}
}
data.currJob.nodeJobSigned.nodeSignedType = type
data.currJob.nodeJobSigned.nodeType = nodeType
data.showNodeSignature = true
},
onJobSignature(currJob) {
doJob.signature(currJob).then(() => {
if (data.currJob.nodeJobSigned.belongType !== '0') {
if (data.currJob.nodeJobSigned.belongType === '1') $message.success('抄送成功')
else $message.success('传阅成功')
data.showJobCopyPass = false
}
else {
$message.success('加签成功')
data.showJobSignature = false
}
if (DIC_PROP.SIGNATURE_TYPE[2].value !== data.currJob.nodeJobSigned.signedType) {
btnMethods.onContextmenuClose()
}
})
},
onNodeSignature(currJob) {
runNode.signature(currJob).then(() => {
$message.success('加节点成功')
data.showNodeSignature = false
if (DIC_PROP.NODE_SIGNATURE_TYPE[2].value !== data.currJob.nodeJobSigned.nodeSignedType) {
btnMethods.onContextmenuClose()
}
})
},
// 选择参与者
openJobRoleUserId(type) {
data.nodeUserType = type
proxy.$refs.userRolePicker.onOpen();
},
onSelectItems(items) {
if (data.nodeUserType === '1') {
btnMethods.onJobNextUserRole(items[0])
} else {
btnMethods.onTurnRunJob(items[0])
}
},
// 提前结束流程
handleEarlyComplete(row) {
useMessageBox().prompt('请输入提前结束流程理由')
.then(({ value }) => {
row.jobBtn = DIC_PROP.JOB_BTNS[16].value
row.invalidReason = value
row.flowStatus = '0'
return runFlow.earlyComplete(row)
}).then(() => {
$message.success('操作成功')
btnMethods.onContextmenuClose()
})
},
// 终止流程
handleTerminateFlow(row) {
useMessageBox().prompt('请输入终止理由')
.then(({ value }) => {
row.jobBtn = DIC_PROP.JOB_BTNS[15].value
row.invalidReason = value
row.flowStatus = '0'
return runFlow.terminateFlow(row)
}).then(() => {
$message.success('操作成功')
btnMethods.onContextmenuClose()
})
},
// 作废流程
handleInvalidFlow(row) {
useMessageBox().prompt('请输入作废理由')
.then(({ value }) => {
row.jobBtn = DIC_PROP.JOB_BTNS[17].value
row.invalidReason = value
row.flowStatus = '0'
return runFlow.invalidFlow(row)
}).then(() => {
$message.success('操作成功')
btnMethods.onContextmenuClose()
})
},
// 转办任务
onTurnRunJob(role) {
useMessageBox()
.confirm('是否确认转办名称为"' + data.currJob.jobName + '"的任务?')
.then(() => {
data.currJob.jobType = role.jobType
data.currJob.roleId = role.roleId
data.currJob.jobBtn = DIC_PROP.JOB_BTNS[14].value
return doJob.turnRunJob(data.currJob)
}).then(() => {
$message.success('操作成功')
btnMethods.onContextmenuClose()
})
},
// 指定下一办理人
onJobNextUserRole(role) {
useMessageBox()
.confirm('是否确认指定下一步参与者?')
.then(() => {
data.currJob.nextUserRole = role
return btnMethods.doHandleJob(DIC_PROP.JOB_BTNS[13].value)
})
},
onContextmenuClose() {
// flowJob.delJobLen()
btnMethods.postMessage()
// 防止重复进入
if (route.fullPath.indexOf('tokenLoaded') === -1) {
let source = { contextMenuClickId: 1, ...route }
mittBus.emit('onCurrentContextmenuClick', Object.assign({}, source));
} else {
let timer = setTimeout(() => {
window.close();
clearTimeout(timer);
}, 2000);
}
},
postMessage() {
channel.postMessage({ type: 'currJob', data: '' });
}
}
const $route = useRoute();
const methods = {
// 初始化数据
async initJobData() {
data.loading = false
let row = { }
if (!validateNull(props.currJob)) row = props.currJob
else {
setPropsDataValue(row, $route.query, 'id', 'flowInstId', 'isHiJob', 'isView', 'isRead', 'isApp', 'isForm')
}
if (row.isHiJob !== '0') {
await handleToDoneDetail(row, data, row.isApp, row.isForm);
} else {
await handleTodoDetail(row, data, row.isView, row.isRead, $message, btnMethods.postMessage);
}
data.jobBtns = data.currJob.jobBtns
methods.handleElTabs()
data.currJob.resolveSaves = []
data.currJob.resolveMethods = []
nextTick(() => {
initHandleJobHeight(data)
})
},
// 处理tabs
handleElTabs() {
data.elTabs = methods.removeCloneElTab()
// 排序
let elTab = data.elTabs.sort((s1, s2) => {
return s1.sort - s2.sort
}).find(f => f.isActive === '1')
// 默认展示的tab
if (elTab) data.currElTab = elTab
else data.currElTab = data.elTabs[0]
data.currElTab.active = data.currElTab.id
data.currJob.currElTab = data.currElTab
},
removeCloneElTab() {
// 判断是否显隐
let clone = deepClone(data.currJob.elTabs)
let conditions = orderVue.handleElTab(data.currJob)
conditions.forEach(cd => {
if (!cd.isShow || cd.isShow === '0') {
data.currJob.elTabs = data.currJob.elTabs.filter(f => f.path !== cd.path);
clone = clone.filter(f => f.path !== cd.path)
}
})
return clone
},
// 页面选择
handleClick(tab: TabsPaneContext) {
if (data.currElTab.id === tab.paneName) return
// 切换页面
data.currElTab = data.elTabs.find(f => f.id === tab.paneName)
data.currElTab.active = data.currElTab.id
data.currJob.currElTab = data.currElTab
// 影藏弹出框滚动条
initHandleJobHeight(data)
},
// 提交任务
async handleJob(jobBtn, isAudit) {
// 提交时自动审批
if (proxy.$refs.signNameRef) {
await proxy.$refs.signNameRef.handleGenerate()
}
let b = jobBtn !== DIC_PROP.JOB_BTNS[0].value
if (b) {
methods.timeoutLoading()
btnMethods.onHandleJob(jobBtn)
return
}
// 审批前回调函数
let resolveMethods = data.currJob.resolveMethods
if (!validateNull(resolveMethods)) {
for (const resolveMethod of resolveMethods) {
if (resolveMethod) {
methods.timeoutLoading()
let isPass = await resolveMethod();
if (isPass !== true) return
}
}
}
// 审批前保存回调函数
let resolveSaves = data.currJob.resolveSaves
if (isAudit && !validateNull(resolveSaves)) {
for (const resolveSave of resolveSaves) {
if (resolveSave) {
methods.timeoutLoading()
await resolveSave();
}
}
}
// 判断页面是否保存
let hiJob = data.currJob.hiJob;
let elTabs = data.currJob.elTabs.filter(f => (f.isFormEdit !== '0' && !hiJob))
if (elTabs) {
let find = elTabs.find(f => f.isSave !== true);
if (find) useMessage().info(find.formName + ' 未保存')
else {
methods.timeoutLoading()
btnMethods.onHandleJob(jobBtn)
}
return
}
methods.timeoutLoading()
btnMethods.onHandleJob(jobBtn)
},
handleJobBtns(jobBtn) {
// TODO 可在此处做全局权限判断
let value = DIC_PROP.JOB_BTNS[13].value;
let next = userInfo.userInfos.authBtnList.some(v => v === 'jsonflow_runjob_invalid');
if (next) {
return (jobBtn === value && next) || jobBtn !== value
} else return jobBtn !== value
},
getJobBtnName(jobBtn) {
return getJobBtnName(jobBtn)
},
timeoutLoading() {
data.loading = true
setTimeout(() => {// 防重复提交
data.loading = false
}, 3 * 1000)
}
}
// 监听双向绑定
watch(
() => props.currJob.id,
(val) => {
methods.initJobData();
}
);
onMounted(() => {
methods.initJobData();
});
</script>
<style lang="scss">
@import "../../../flow/components/style/flow-drawer.scss";
</style>

View File

@@ -0,0 +1,130 @@
import * as doJob from "/@/api/jsonflow/do-job";
import {useMessageBox} from "/@/hooks/message";
import {DIC_PROP} from "/@/flow/support/dict-prop";
import * as runFlow from "/@/api/jsonflow/run-flow";
import {appointUser} from "/@/api/jsonflow/do-job";
// 签收反签收任务
export function handleSignForJob(methods, $message, getDataList, row, type) {
let obj = {id: row.id, signForType: type};
if (type === '0') {
// 判断反签收
methods.onSignForJob(obj)
return
}
doJob.signForJob(obj)
.then(() => {
$message.success('操作成功');
getDataList();
})
}
// 反签收任务
export function onSignForJob($message, flowJob, getDataList, obj) {
let msg = '是否确认反签收当前任务, 将放回待认领列表?'
useMessageBox()
.confirm(msg)
.then(() => {
return doJob.signForJob(obj)
}).then(() => {
$message.success('操作成功')
flowJob.delJobLen();
getDataList();
})
}
// 挂起激活任务
export function handleSuspension($message, getDataList, row, suspension) {
if (suspension === '0') {
row.suspension = suspension
doJob.suspension(row).then(() => {
$message.success('操作成功');
getDataList();
})
return
}
// 增加挂起原因
useMessageBox()
.prompt('请输入挂起原因').then(({value}) => {
row.suspension = suspension
row.suspensionReason = value
return doJob.suspension(row)
}).then(() => {
$message.success('操作成功');
getDataList();
})
}
// 提前结束流程
export function handleEarlyComplete($message, flowJob, getDataList, row) {
useMessageBox().prompt('请输入提前结束流程理由')
.then(({ value }) => {
row.jobBtn = DIC_PROP.JOB_BTNS[16].value
row.invalidReason = value
row.flowStatus = '0'
return runFlow.earlyComplete(row)
}).then(() => {
$message.success('操作成功')
flowJob.delJobLen();
getDataList();
})
}
// 终止流程
export function handleTerminateFlow($message, flowJob, getDataList, row) {
useMessageBox().prompt('请输入终止理由')
.then(({ value }) => {
row.jobBtn = DIC_PROP.JOB_BTNS[15].value
row.invalidReason = value
row.flowStatus = '0'
return runFlow.terminateFlow(row)
}).then(() => {
$message.success('操作成功')
flowJob.delJobLen();
getDataList();
})
}
// 作废流程
export function handleInvalidFlow($message, flowJob, getDataList, row) {
useMessageBox().prompt('请输入作废理由')
.then(({ value }) => {
row.jobBtn = DIC_PROP.JOB_BTNS[17].value
row.invalidReason = value
row.flowStatus = '0'
return runFlow.invalidFlow(row)
}).then(() => {
$message.success('操作成功')
flowJob.delJobLen();
getDataList();
})
}
// 转办任务
export function onTurnRunJob($message, flowJob, getDataList, data, role) {
useMessageBox()
.confirm('是否确认转办名称为"' + data.currJob.jobName + '"的任务?')
.then(() => {
data.currJob.jobType = role.jobType
data.currJob.roleId = role.roleId
data.currJob.jobBtn = DIC_PROP.JOB_BTNS[14].value
return doJob.turnRunJob(data.currJob)
}).then(() => {
$message.success('操作成功')
flowJob.delJobLen();
getDataList();
})
}
// 指定任务参与者
export function onJobNextUserRole($message, items, getDataList, data) {
useMessageBox()
.confirm('是否确认指派该参与者?')
.then(() => {
data.currJob.nextUserRole = items[0]
appointUser(data.currJob).then(() => {
$message.success('操作成功');
getDataList();
})
})
}

View File

@@ -0,0 +1,188 @@
<template>
<div>
<el-form ref="dataFormRef" :model="form" :rules="dataRules" label-width="150px" v-loading="loading" :disabled="operType === 'view'">
<span style="margin-left: 30px">什么是加签减签加签指在同一个节点里增加其他参与者参与审批加签分为前加签后加签加并签<br/><span style="margin-left: 30px">1前加签即在当前审批人之前增加一个参与者参与审批该参与者审批完成之后会回到当前审批人</span>
<br/><span style="margin-left: 30px">2后加签即在当前审批人之后增加一个参与者参与审批当前审批人审批之后流转到该参与者</span><br/><span style="margin-left: 30px">3加并签即与当前审批人同时审批</span><br/><span style="margin-left: 30px">4减签即减少参与者</span></span>
<el-row :gutter="24">
<el-divider> 加签设置 </el-divider>
<el-col :span="12" class="mb20">
<el-form-item label="在当前节点加签" prop="runNodeId">
<el-select v-model="form.runNodeId" placeholder="默认当前节点" clearable filterable disabled>
<el-option v-for="(item, index) in cascadeDic.runNodeId" :key="index" :label="item.nodeName" :value="item.id"></el-option>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12" class="mb20">
<el-form-item :label="t('runJob.jobName')" prop="jobName">
<el-input v-model="form.jobName" :placeholder="t('runJob.inputJobNameTip')"/>
</el-form-item>
</el-col>
<el-col :span="12" class="mb20">
<el-form-item label="加签参与者类型" prop="jobType">
<el-radio-group @change="handleRoleType"
v-model="form.jobType">
<el-radio v-for="(item, index) in DIC_PROP.JOB_USER_TYPE" :key="index" :label="item.value">
{{ item.label }}
</el-radio>
</el-radio-group>
</el-form-item>
</el-col>
<el-col :span="12" class="mb20" v-if="form.jobType === DIC_PROP.JOB_USER_TYPE[0].value">
<el-form-item label="指定加签人员" prop="roleId">
<el-select v-model="form.roleId" placeholder="请输入用户名称进行模糊搜索" clearable filterable
remote :remote-method="methodsRemote.remoteMethodUser" :reserve-keyword="false">
<el-option v-for="(item, index) in dicData.users" :key="index" :label="item.name" :value="item.userId"></el-option>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12" class="mb20" v-if="form.jobType === DIC_PROP.JOB_USER_TYPE[1].value">
<el-form-item label="指定加签角色" prop="roleId">
<el-select v-model="form.roleId" placeholder="请输入角色名称进行模糊搜索" clearable filterable
remote :remote-method="methodsRemote.remoteMethodRole" :reserve-keyword="false">
<el-option v-for="(item, index) in dicData.roles" :key="index" :label="item.roleName" :value="item.roleId"></el-option>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12" class="mb20" v-if="form.jobType === DIC_PROP.JOB_USER_TYPE[2].value">
<el-form-item label="指定加签岗位" prop="roleId">
<el-select v-model="form.roleId" placeholder="请输入岗位名称进行模糊搜索" clearable filterable
remote :remote-method="methodsRemote.remoteMethodPost" :reserve-keyword="false">
<el-option v-for="(item, index) in dicData.posts" :key="index" :label="item.postName" :value="item.postId"></el-option>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12" class="mb20" v-if="form.jobType === DIC_PROP.JOB_USER_TYPE[3].value">
<el-form-item label="指定加签部门" prop="roleId">
<el-select v-model="form.roleId" placeholder="请输入部门名称进行模糊搜索" clearable filterable
remote :remote-method="methodsRemote.remoteMethodDept" :reserve-keyword="false">
<el-option v-for="(item, index) in dicData.depts" :key="index" :label="item.name" :value="item.deptId"></el-option>
</el-select>
</el-form-item>
</el-col>
</el-row>
</el-form>
<footer class="el-dialog__footer">
<span class="dialog-footer">
<el-button type="primary" @click="handleUpdate" :loading="loading">{{ $t('common.confirmButtonText') }}</el-button>
</span>
</footer>
</div>
</template>
<script setup lang="ts" name="JobSignature">
import { useI18n } from "vue-i18n"
import {onCascadeChange, onLoadDicUrl, remoteMethodByKey} from "../convert-name/convert";
import {useMessage} from "/@/hooks/message";
import {handleChangeJobType} from "../../index";
const { t } = useI18n();
const $message = useMessage();
const $emit = defineEmits(['onJobSignature']);
// 定义变量内容
const dataFormRef = ref();
const loading = ref(false);
const operType = ref(false);
// 定义字典
const dicData = reactive({});
const cascadeDic = reactive({});
const onLoad = onLoadDicUrl();
const onCascade = onCascadeChange(cascadeDic, {key: "flowInstId", cascades: ["runNodeId"]});
onMounted(() => {
// onLoad(dicData);
});
// 提交表单数据
const form = reactive({
runNodeId: '',
jobType: '',
roleId: null,
});
// 定义校验规则
const dataRules = ref({
jobType: [{required: true, message: '加签参与者类型不能为空', trigger: 'blur'}],
roleId: [{required: true, message: '加签参与者不能为空', trigger: 'blur'}],
})
const props = defineProps({
currJob: {
default: null,
}
});
function initJobData() {
Object.assign(form, props.currJob)
form.roleId = null
onCascade(form)
}
function handleRoleType() {
handleChangeJobType(dicData, form)
}
const methodsRemote = {
remoteMethodUser(query: string) {
remoteMethodByKey(query, onLoad, dicData, 'userName', "users")
},
remoteMethodRole(query: string) {
remoteMethodByKey(query, onLoad, dicData, 'roleName', "roles")
},
remoteMethodPost(query: string) {
remoteMethodByKey(query, onLoad, dicData, 'postName', "posts")
},
remoteMethodDept(query: string) {
remoteMethodByKey(query, onLoad, dicData, 'deptName', "depts")
},
}
async function handleUpdate() {
const valid = await dataFormRef.value.validate().catch(() => {});
if (!valid) return false;
form.nextUserRole = {jobType: form.jobType, roleId: form.roleId, jobName: form.jobName}
props.currJob.nextUserRole = form.nextUserRole
loading.value = true
try {
$emit('onJobSignature', props.currJob)
setTimeout(() => { // 异步异常
loading.value = false
}, 3000)
} catch (e) {
loading.value = false
}
}
// 监听双向绑定
watch(
() => props.currJob.id,
(val) => {
initJobData();
}
);
onMounted(() => {
initJobData();
});
</script>
<style lang="scss" scoped>
.el-dialog__footer {
text-align: center;
.dialog-footer {
text-align: center;
}
}
</style>

View File

@@ -0,0 +1,224 @@
<template>
<div>
<el-form ref="dataFormRef" :model="form" :rules="dataRules" label-width="150px" v-loading="loading" :disabled="operType === 'view'">
<span style="margin-left: 30px">什么是加减节点加节点指在流程图里增加其他节点及参与者参与审批加节点分为前加节点后加节点加并节点 <br/><span style="margin-left: 30px">1前加节点即在当前审批人节点之前增加一个节点及参与者参与审批该参与者节点审批完成之后会回到当前审批人节点</span>
<br/><span style="margin-left: 30px">2后加节点即在当前审批人节点之后增加一个节点及参与者参与审批当前审批人节点审批之后流转到该参与者节点</span><br/><span style="margin-left: 30px">3加并节点即与当前审批人节点在同一个节点层级若当前审批人节点为会签节点则需所有节点通过</span>
若当前审批人节点为或签节点则一个节点通过即可若当前审批人节点为依次审批则按顺序依次通过即可<br/><span style="margin-left: 30px">4减节点即减少节点及参与者</span></span>
<el-row :gutter="24">
<el-divider> 加节点设置 </el-divider>
<el-col :span="12" class="mb20">
<el-form-item label="基于当前节点加节点" prop="runNodeId">
<el-select v-model="form.runNodeId" placeholder="默认当前节点" clearable filterable disabled>
<el-option v-for="(item, index) in cascadeDic.runNodeId" :key="index" :label="item.nodeName" :value="item.id"></el-option>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12" class="mb20">
<el-form-item :label="t('runNode.nodeName')" :rules="{ required: true, message: '节点名称不能为空', trigger: 'blur' }">
<el-input v-model="form.nodeJobSigned.nodeName" :placeholder="t('runNode.inputNodeNameTip')"/>
</el-form-item>
</el-col>
<el-col :span="12" class="mb20">
<el-form-item label="加节点参与者类型" prop="jobType">
<el-radio-group @change="handleRoleType"
v-model="form.jobType">
<el-radio v-for="(item, index) in DIC_PROP.JOB_USER_TYPE" :key="index" :label="item.value">
{{ item.label }}
</el-radio>
</el-radio-group>
</el-form-item>
</el-col>
<el-col :span="24" class="mb20" v-if="props.currJob.runNodeVO.nodeApproveMethod === DIC_PROP.NODE_APPROVE_METHOD[2].value
&& props.currJob.nodeJobSigned.nodeSignedType === DIC_PROP.NODE_SIGNATURE_TYPE[2].value">
<el-form-item label="依次审批节点顺序" prop="signedType">
<el-radio-group v-model="form.nodeJobSigned.signedType">
<el-radio v-for="(item, index) in DIC_PROP.NODE_SEQUENTIAL_TYPE" :key="index" :label="item.value">
{{ item.label }}
</el-radio>
</el-radio-group>
</el-form-item>
</el-col>
<el-col :span="12" class="mb20" v-if="form.jobType === DIC_PROP.JOB_USER_TYPE[0].value">
<el-form-item label="指定参与者" prop="roleId">
<el-tooltip content="请输入用户名称进行模糊搜索" placement="top">
<el-select v-model="form.roleId" placeholder="请输入用户名称进行模糊搜索" clearable filterable
remote :remote-method="methodsRemote.remoteMethodUser" :reserve-keyword="false">
<el-option v-for="(item, index) in dicData.users" :key="index" :label="item.name" :value="item.userId"></el-option>
</el-select>
</el-tooltip>
</el-form-item>
</el-col>
<el-col :span="12" class="mb20" v-if="form.jobType === DIC_PROP.JOB_USER_TYPE[1].value">
<el-form-item label="指定参与者" prop="roleId">
<el-tooltip content="请输入角色名称进行模糊搜索" placement="top">
<el-select v-model="form.roleId" placeholder="请输入角色名称进行模糊搜索" clearable filterable
remote :remote-method="methodsRemote.remoteMethodRole" :reserve-keyword="false">
<el-option v-for="(item, index) in dicData.roles" :key="index" :label="item.roleName" :value="item.roleId"></el-option>
</el-select>
</el-tooltip>
</el-form-item>
</el-col>
<el-col :span="12" class="mb20" v-if="form.jobType === DIC_PROP.JOB_USER_TYPE[2].value">
<el-form-item label="指定参与者" prop="roleId">
<el-tooltip content="请输入岗位名称进行模糊搜索" placement="top">
<el-select v-model="form.roleId" placeholder="请输入岗位名称进行模糊搜索" clearable filterable
remote :remote-method="methodsRemote.remoteMethodPost" :reserve-keyword="false">
<el-option v-for="(item, index) in dicData.posts" :key="index" :label="item.postName" :value="item.postId"></el-option>
</el-select>
</el-tooltip>
</el-form-item>
</el-col>
<el-col :span="12" class="mb20" v-if="form.jobType === DIC_PROP.JOB_USER_TYPE[3].value">
<el-form-item label="指定参与者" prop="roleId">
<el-tooltip content="请输入部门名称进行模糊搜索" placement="top">
<el-select v-model="form.roleId" placeholder="请输入部门名称进行模糊搜索" clearable filterable
remote :remote-method="methodsRemote.remoteMethodDept" :reserve-keyword="false">
<el-option v-for="(item, index) in dicData.depts" :key="index" :label="item.name" :value="item.deptId"></el-option>
</el-select>
</el-tooltip>
</el-form-item>
</el-col>
<el-col :span="12" class="mb20">
<el-form-item :label="t('runNode.nodeType')" prop="nodeType">
<el-select v-model="form.nodeJobSigned.nodeType" :placeholder="t('runNode.inputNodeTypeTip')" filterable>
<el-option v-for="(item, index) in DIC_PROP.NODE_TYPE.slice(1, 3)" :key="index" :label="item.label" :value="item.value"></el-option>
</el-select>
</el-form-item>
</el-col>
</el-row>
</el-form>
<footer class="el-dialog__footer">
<span class="dialog-footer">
<el-button type="primary" @click="handleUpdate" :loading="loading">{{ $t('common.confirmButtonText') }}</el-button>
</span>
</footer>
</div>
</template>
<script setup lang="ts" name="JobSignature">
import { useI18n } from "vue-i18n"
import {onCascadeChange, onLoadDicUrl, remoteMethodByKey} from "../convert-name/convert";
import {useMessage} from "/@/hooks/message";
import {handleChangeJobType} from "../../index";
const { t } = useI18n();
const $message = useMessage();
const $emit = defineEmits(['onNodeSignature']);
// 定义变量内容
const dataFormRef = ref();
const loading = ref(false);
const operType = ref(false);
// 定义字典
const dicData = reactive({});
const cascadeDic = reactive({});
const onLoad = onLoadDicUrl();
const onCascade = onCascadeChange(cascadeDic, {key: "flowInstId", cascades: ["runNodeId"]});
onMounted(() => {
// onLoad(dicData);
});
// 提交表单数据
const form = reactive({
runNodeId: null,
jobType: '',
roleId: null,
nodeJobSigned: {},
});
// 定义校验规则
const dataRules = ref({
jobType: [{required: true, message: '参与者类型不能为空', trigger: 'blur'}],
roleId: [{required: true, message: '参与者不能为空', trigger: 'blur'}],
// nodeName: [{required: true, message: '节点名称不能为空', trigger: 'blur'}],
})
const props = defineProps({
currJob: {
default: null,
}
});
function initJobData() {
Object.assign(form, props.currJob)
form.roleId = null
onCascade(form)
}
function handleRoleType() {
handleChangeJobType(dicData, form)
}
const methodsRemote = {
remoteMethodUser(query: string) {
remoteMethodByKey(query, onLoad, dicData, 'userName', "users")
},
remoteMethodRole(query: string) {
remoteMethodByKey(query, onLoad, dicData, 'roleName', "roles")
},
remoteMethodPost(query: string) {
remoteMethodByKey(query, onLoad, dicData, 'postName', "posts")
},
remoteMethodDept(query: string) {
remoteMethodByKey(query, onLoad, dicData, 'deptName', "depts")
},
}
async function handleUpdate() {
// TOD 上面两种方式都校验不了
if (!form.nodeJobSigned.nodeName) {
$message.error("节点名称不能为空");
return
}
const valid = await dataFormRef.value.validate().catch(() => {});
if (!valid) return false;
form.nextUserRole = {jobType: form.jobType, roleId: form.roleId}
props.currJob.nextUserRole = form.nextUserRole
props.currJob.nodeJobSigned = form.nodeJobSigned
loading.value = true
try {
$emit('onNodeSignature', props.currJob)
setTimeout(() => { // 异步异常
loading.value = false
}, 3000)
} catch (e) {
loading.value = false
}
}
// 监听双向绑定
watch(
() => props.currJob.id,
(val) => {
initJobData();
}
);
onMounted(() => {
initJobData();
});
</script>
<style lang="scss" scoped>
.el-dialog__footer {
text-align: center;
.dialog-footer {
text-align: center;
}
}
</style>

View File

@@ -0,0 +1,43 @@
export default {
jfI18n: {
temp: 'Temp',
invalid: 'Invalid',
pub: 'Publish',
oneBtnDesign: 'OneBtn Design',
viewPageBtn: 'View Page',
onceMoreDesign: 'Once More Design',
designFlow: 'Design Flow',
viewFlow: 'View Flow',
print: 'Print',
printTemplate: 'Print Template',
submit: 'Submit',
viewForm: 'View Form',
initHandover: 'Initial Handover',
initialBtn: 'Initiate',
batchBtn: 'Batch Save',
saveBtn: 'Save',
cancelBtn: 'Cancel',
updateFlow: 'Update Flow',
rejectBtn: 'Reject Check',
receiveBtn: 'Confirm Receive',
recallBtn: 'Recall',
resetBtn: 'Reset',
operate: 'Operate',
more: 'More',
},
jfAttr: {
paramValType: 'Parameter Value Type',
varKeyVal: 'Form Field OR Fixed Value',
sysVarKeyVal: 'Sys Field OR Fixed Value',
subVarKeyVal: 'Subform Field',
subVarKeyVal2: 'Subform Field OR Fixed Value',
requestHeader: 'Request Header',
requestParam: 'Request Param',
returnParam: 'Return Param',
},
jfForm: {
urgentTask: 'Urgent task',
urgentSubFlow: 'Urgent Sub Flow',
reduceSign: 'Reduce Sign'
}
}

View File

@@ -0,0 +1,43 @@
export default {
jfI18n: {
temp: '暂存',
invalid: '作废',
pub: '发布',
oneBtnDesign: '一键设计',
viewPageBtn: '查看表单',
onceMoreDesign: '再次设计',
designFlow: '设计流程图',
viewFlow: '查看流程图',
print: '打印',
printTemplate: '打印设计',
submit: '提交',
viewForm: '查看表单',
initHandover: '发起交接',
initialBtn: '发起',
batchBtn: '批量保存',
saveBtn: '保存',
cancelBtn: '取消',
updateFlow: '修改工单',
rejectBtn: '驳回选中项',
receiveBtn: '确认接收',
recallBtn: '撤回',
resetBtn: '重发',
operate: '操作',
more: '更多',
},
jfAttr: {
paramValType: '参数值类型',
varKeyVal: '表单字段或固定值',
sysVarKeyVal: '系统字段或固定值',
subVarKeyVal: '子流程表单字段',
subVarKeyVal2: '子流程表单字段或固定值',
requestHeader: '请求头',
requestParam: '请求参数',
returnParam: '返回值',
},
jfForm: {
urgentTask: '催办任务',
urgentSubFlow: '催办子流程',
reduceSign: '减签'
}
}

View File

@@ -0,0 +1,42 @@
import ConvertName from './convert-name/index.vue';
import ConvertRoleName from './convert-name/role-index.vue';
import ConvertGroupName from './convert-name/group-index.vue';
import {App} from "vue";
import UserPicker from "./user-role/user.vue";
import RolePicker from "./user-role/role.vue";
import PostPicker from "./user-role/post.vue";
import DeptPicker from "./user-role/dept.vue";
import PhoneInput from "./form-create/phone.vue";
import IdCartInput from "./form-create/id-card.vue";
import FlowNameInput from "./form-create/flow-name.vue";
import FormNameInput from "./form-create/form-name.vue";
import FormCodeInput from "./form-create/form-code.vue";
import EmailInput from "./form-create/email.vue";
import SignInput from "./sign/index.vue";
// vite glob导入
const modules: Record<string, () => Promise<unknown>> = import.meta.glob(['../../views/jsonflow/*/*.vue', '../../views/order/*/*.vue'])
/**
* 导出全局注册工作流审批表单组件
* @param app vue 实例
*/
function dynamicImport(app: App) {
for (const module in modules) {
// @ts-ignore
app.component(module, defineAsyncComponent(modules[module]));
}
app.component('UserPicker', UserPicker);
app.component('RolePicker', RolePicker);
app.component('PostPicker', PostPicker);
app.component('DeptPicker', DeptPicker);
app.component('PhoneInput', PhoneInput);
app.component('IdCartInput', IdCartInput);
app.component('FlowNameInput', FlowNameInput);
app.component('FormNameInput', FormNameInput);
app.component('FormCodeInput', FormCodeInput);
app.component('EmailInput', EmailInput);
app.component('SignInput', SignInput);
}
export { ConvertName, ConvertRoleName, ConvertGroupName, dynamicImport };

View File

@@ -0,0 +1,180 @@
<template>
<div class="layout-padding">
<el-container>
<el-header style="background: white">
<flow-design-header :currFlowForm="props.currFlowForm" @handleDesignFlow="methods.handleDesignFlow"
@syncCurrFlowForm="methods.syncCurrFlowForm"
@publish="methods.publishFlow"></flow-design-header>
</el-header>
<div style="min-width: 980px;">
<form-design :currFlowForm="props.currFlowForm" ref="formDesign" v-show="methods.showFormDesign()"/>
<form-def-perm :currFlowForm="props.currFlowForm" ref="formDefPerm" v-show="methods.showFormDefPerm()"/>
</div>
<div id="initClientHeight">
<form-setting :currFlowForm="props.currFlowForm" ref="formSetting" v-show="props.currFlowForm.active === 'formSetting'"/>
<flow-design :currFlowForm="props.currFlowForm" ref="flowDesign" v-show="props.currFlowForm.active === 'flowDesign'"/>
</div>
</el-container>
</div>
</template>
<script setup lang="ts" name="FlowFormDesign">
import {useI18n} from "vue-i18n"
import {useMessageBox} from "/@/hooks/message";
import {notifyLeft, validateRunFlowId} from "/@/flow";
import {confirmCancelAndClose, formWidgetDesignHeight, handleUpgradeVersion} from "/@/flow/utils";
import {DIC_PROP} from "../../support/dict-prop";
const {t} = useI18n();
const {proxy} = getCurrentInstance();
// 引入组件
const FlowDesignHeader = defineAsyncComponent(() => import('./header.vue'));
const FormDesign = defineAsyncComponent(() => import('../form-create/designer.vue'));
const FlowDesign = defineAsyncComponent(() => import('/@/views/jsonflow/flow-design/index.vue'));
const FormSetting = defineAsyncComponent(() => import('./setting.vue'));
const FormDefPerm = defineAsyncComponent(() => import('/@/views/jsonflow/form-option/form-def-perm.vue'));
const $emit = defineEmits(['handleDesignFlow']);
const props = defineProps({
currFlowForm: {
type: Object,
default: null,
}
});
const methods = {
handleDesignFlow(bool) {
$emit("handleDesignFlow", bool);
},
initClientHeight() {
nextTick(() => {
let browserHeight = window.innerHeight || document.documentElement.clientHeight || document.body.clientHeight;
formWidgetDesignHeight(browserHeight);
})
},
showFormDesign() {
return props.currFlowForm.active === 'formDesign' && props.currFlowForm.type !== DIC_PROP.FORM_TYPE[1].value
},
showFormDefPerm() {
return props.currFlowForm.active === 'formDesign' && props.currFlowForm.type === DIC_PROP.FORM_TYPE[1].value
},
initCurrFlowInfo(active) {
nextTick(() => {
window._jfOperate.reAutoInitNodes();
methods.syncCurrFlowInfo(active)
})
},
syncCurrFlowInfo(active, isInit?) {
let attrs = methods.refsFlowDataAttrs();
if (active !== 'flowDesign' || isInit) {
attrs.flowKey = props.currFlowForm.flowKey
attrs.groupName = props.currFlowForm.groupName
attrs.flowName = props.currFlowForm.formName
} else {
if (attrs.flowKey) {
props.currFlowForm.defFlowId = attrs.id
props.currFlowForm.flowKey = attrs.flowKey
props.currFlowForm.groupName = attrs.groupName
props.currFlowForm.formName = attrs.flowName
}
}
validateRunFlowId(props, attrs)
},
syncCurrFormInfo(callback?) {
if (props.currFlowForm.type !== DIC_PROP.FORM_TYPE[1].value) {
proxy.$refs.formDefPerm.syncCurrFormInfo(true)
// 保存表单信息
proxy.$refs.formDesign.handleSubmit(() => {
if (callback) callback()
});
} else {
proxy.$refs.formDefPerm.syncCurrFormInfo(false)
props.currFlowForm.formInfo = null
}
},
refsFlowDataAttrs() {
// 每次取最新
return proxy.$refs.flowDesign.flowData.attrs;
},
async syncCurrFlowForm(menu?, active?, callback?) {
let preSave = await proxy.$refs.formSetting.validatePreSave();
if (callback) callback(preSave)
if (!preSave) return
if (active === 'formDesign') methods.syncCurrFormInfo();
let defFlowId = props.currFlowForm.defFlowId;
let flowInstId = props.currFlowForm.flowInstId;
if (menu === 'flowDesign') {
let attrs = methods.refsFlowDataAttrs();
if (defFlowId && !attrs.flowKey) {
// 切换到才初始化
proxy.$refs.flowDesign.initFlow(defFlowId, flowInstId, () => {
methods.syncCurrFlowInfo(menu, true)
});
} else methods.initCurrFlowInfo(active)
} else if (active === 'flowDesign'){
methods.syncCurrFlowInfo(active)
}
},
async publishFlow(menu, callback, status, version) {
if (version === true) {
try {
if (props.currFlowForm.isNew) {
notifyLeft('当前设计版本已最新, 请确认是否已保存')
return
}
await useMessageBox().confirm('是否确定升级版本?');
} catch {
return;
}
await handleUpgradeVersion(props)
// 升级流程信息
proxy.$refs.flowDesign.initNewFlow(() => {
methods.syncCurrFlowInfo('flowDesign')
let confirmObj = {text: "发布流程", callback: () => { methods.publishFlow(menu, callback, '1', null) }}
let cancelObj = {text: "暂存流程", callback: () => { methods.publishFlow(menu, callback, '-1', null) }}
confirmCancelAndClose(confirmObj, cancelObj, '流程设计升级版本成功! 是否立即暂存或发布?')
}, true);
return
}
methods.syncCurrFormInfo(callback);
// 校验表单设计
let otherSave = await proxy.$refs.formSetting.validateOtherSave();
if (!otherSave) return
// 保存流程信息
props.currFlowForm.flowDesigning = true
proxy.$refs.flowDesign.publishFlow(() => {
methods.syncCurrFlowInfo('flowDesign')
methods.submitFormSetting(callback, status)
if (callback) callback()
}, status);
},
async submitFormSetting(callback, status) {
// 保存字段信息
if (props.currFlowForm.type === DIC_PROP.FORM_TYPE[1].value) {
await proxy.$refs.formDefPerm.handleSubmit(() => {
props.currFlowForm.formDesign = true
if (callback) callback()
});
}
await proxy.$refs.formSetting.handleSubmit(() => {
if (callback) callback()
}, status)
}
}
// 监听双向绑定
watch(
() => props.currFlowForm.id,
(val) => {
methods.initClientHeight()
}
);
onMounted(() => {
methods.initClientHeight();
});
</script>
<style lang="scss" scoped>
</style>

View File

@@ -0,0 +1,312 @@
<template>
<el-dialog :title="title" v-model="visible" width="80%"
:close-on-click-modal="false" draggable>
<el-form ref="dataFormRef" :model="form" :rules="dataRules" label-width="130px" v-loading="loading" :disabled="operType === 'view'">
<el-row :gutter="24">
<el-col :span="12" class="mb20">
<el-form-item :label="t('flowApplication.flowKey')" prop="flowKey">
<el-input v-model="form.flowKey" :placeholder="t('flowApplication.inputFlowKeyTip')" clearable/>
</el-form-item>
</el-col>
<el-col :span="12" class="mb20">
<el-form-item :label="t('flowApplication.formName')" prop="formName">
<el-input v-model="form.formName" :placeholder="t('flowApplication.inputFormNameTip')" maxlength="20" show-word-limit clearable/>
</el-form-item>
</el-col>
<el-col :span="12" class="mb20">
<el-form-item :label="t('flowApplication.icon')" prop="icon">
<IconSelector :placeholder="$t('flowApplication.inputIconTip')" v-model="form.icon" />
</el-form-item>
</el-col>
<el-col :span="12" class="mb20">
<el-form-item :label="t('flowApplication.groupName')" prop="groupName">
<el-select v-model="form.groupName" :placeholder="t('flowApplication.inputGroupNameTip')" style="width: 80%!important;"
clearable filterable allowCreate defaultFirstOption>
<el-option v-for="(item, index) in dicData.groupName" :key="index" :label="item.groupName" :value="item.groupName"></el-option>
</el-select>
<el-button
type="primary" size="small" round style="margin-left: 10px"
@click="handleAddGroupName"
>新增分组
</el-button>
</el-form-item>
</el-col>
<el-col :span="12" class="mb20">
<el-form-item :label="t('flowApplication.type')" prop="type">
<el-select v-model="form.type" :placeholder="t('flowApplication.inputTypeTip')" clearable filterable>
<el-option v-for="(item, index) in DIC_PROP.FORM_TYPE" :key="index" :label="item.label" :value="item.value"></el-option>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12" class="mb20" v-if="form.type === DIC_PROP.FORM_TYPE[1].value">
<el-form-item :label="t('tabsOption.path')" prop="path">
<el-input v-model="form.path" :placeholder="t('tabsOption.inputPathTip')" clearable>
</el-input>
</el-form-item>
</el-col>
<el-col :span="12" class="mb20">
<el-form-item :label="t('flowApplication.tableName')" prop="tableName">
<el-tooltip content="若自行调用接口保存表单数据,则此处需为空" placement="top">
<el-select v-model="form.tableName" :placeholder="t('flowApplication.inputTableNameTip')" clearable filterable>
<el-option v-for="(item, index) in dicData.tableName" :key="index" :label="item.tableName" :value="item.tableName"></el-option>
</el-select>
</el-tooltip>
</el-form-item>
</el-col>
<el-col :span="12" class="mb20">
<el-form-item :label="t('flowApplication.permission')" prop="permission">
<el-tooltip content="请输入角色名称进行模糊搜索,默认所有人都可以发起该流程" placement="top">
<el-select v-model="form.permission" :placeholder="t('flowApplication.inputPermissionTip')" clearable filterable multiple
remote :remote-method="remoteMethod" :reserve-keyword="false">
<el-option v-for="(item, index) in dicData.permission" :key="index" :label="item.roleName" :value="item.roleId"></el-option>
</el-select>
</el-tooltip>
</el-form-item>
</el-col>
<el-col :span="12" class="mb20">
<el-form-item :label="t('flowApplication.subTableName')" prop="subTableName">
<el-tooltip content="当存在多个关联子表时,多个子表名称名顺序与《关联子表属性》顺序一一对应" placement="top">
<el-select v-model="form.subTableName" :placeholder="t('flowApplication.inputSubTableNameTip')"
@change="changeSubTableName" clearable filterable multiple>
<el-option v-for="(item, index) in dicData.tableName" :key="index" :label="item.tableName" :value="item.tableName"></el-option>
</el-select>
</el-tooltip>
</el-form-item>
</el-col>
<el-col :span="12" class="mb20" v-if="!validateNull(form.subTableName)">
<el-tooltip content="当关联子表名称存在时可配置子表中关联【主表主键】的列名如main_id" placement="top">
<el-form-item :label="t('flowApplication.subMainField')" prop="subMainField">
<el-input v-model="form.subMainField" :placeholder="t('flowApplication.inputSubMainFieldTip')"></el-input>
</el-form-item>
</el-tooltip>
</el-col>
<el-col :span="12" class="mb20" v-if="!validateNull(form.subTableName)">
<el-tooltip content="当关联子表名称存在时可配置主表中关联【子表集合数据】的属性名如subFormList。多个子表属性名以逗号分割与《关联子表名称》顺序一一对应" placement="top">
<el-form-item :label="t('flowApplication.mainSubProp')" prop="mainSubProp">
<el-input v-model="form.mainSubProp" :placeholder="t('flowApplication.inputMainSubPropTip')"></el-input>
</el-form-item>
</el-tooltip>
</el-col>
<el-col :span="12" class="mb20">
<el-form-item :label="t('flowApplication.remark')" prop="remark">
<el-input v-model="form.remark" type="textarea" :placeholder="t('flowApplication.inputRemarkTip')"/>
</el-form-item>
</el-col>
<el-col :span="12" class="mb20">
<el-form-item :label="t('flowApplication.sort')" prop="sort">
<el-input-number :min="1" :max="1000" v-model="form.sort" :placeholder="t('flowApplication.inputSortTip')"></el-input-number>
</el-form-item>
</el-col>
<el-col :span="12" class="mb20">
<el-form-item :label="t('tabsOption.isActive')" prop="isActive">
<el-radio-group v-model="form.isActive">
<el-radio v-for="(item, index) in DIC_PROP.YES_OR_NO" :key="index" :label="item.value" >
{{ getLabelByLanguage(item) }}
</el-radio>
</el-radio-group>
</el-form-item>
</el-col>
<el-col :span="12" class="mb20">
<el-form-item :label="t('tabsOption.isAutoAudit')" prop="isAutoAudit">
<el-radio-group v-model="form.isAutoAudit">
<el-radio v-for="(item, index) in DIC_PROP.YES_OR_NO" :key="index" :label="item.value" >
{{ getLabelByLanguage(item) }}
</el-radio>
</el-radio-group>
</el-form-item>
</el-col>
<el-col :span="12" class="mb20">
<el-form-item :label="t('flowApplication.status')" prop="status">
<el-radio-group v-model="form.status">
<el-radio v-for="(item, index) in DIC_PROP.TEMP_STATUS" :key="index" :label="item.value" >
{{ getLabelByLanguage(item) }}
</el-radio>
</el-radio-group>
</el-form-item>
</el-col>
</el-row>
</el-form>
<template #footer v-if="operType !== 'view'">
<span class="dialog-footer">
<el-button @click="visible = false">{{ $t('common.cancelButtonText') }}</el-button>
<el-button type="primary" @click="onSubmit" :disabled="loading">{{ $t('common.confirmButtonText') }}</el-button>
</span>
</template>
</el-dialog>
</template>
<script setup lang="ts" name="FlowApplicationDialog">
import {useMessage, useMessageBox} from "/@/hooks/message";
import { getObj, addObj, putObj } from '/@/api/order/flow-application'
import { useI18n } from "vue-i18n"
import {rule, validateNull} from '/@/utils/validate';
import {
onFormLoadedUrl,
onLoadDicUrl,
onUpdateDicData,
remoteMethodByKey
} from "/@/flow/components/convert-name/convert";
import {notifyLeft} from "/@/flow";
import {setPropsNull} from "../../support/common";
import {DIC_PROP} from "../../support/dict-prop";
import {validateFormTypeSave, vueKey} from "/@/api/order/order-key-vue";
const emit = defineEmits(['refresh']);
const { t } = useI18n();
// 引入组件
const IconSelector = defineAsyncComponent(() => import('/@/components/IconSelector/index.vue'));
// 定义变量内容
const dataFormRef = ref();
const visible = ref(false);
const loading = ref(false);
const operType = ref(false);
const title = ref('');
// 定义字典
const dicData = reactive({});
const onLoad = onLoadDicUrl({key: "groupName"}, {key: "tableName"});
const onUpdate = onUpdateDicData({key: "groupName"});
const onFormLoaded = onFormLoadedUrl({key: "permission"});
onMounted(() => {
onLoad(dicData);
});
// 提交表单数据
const form = reactive({
flowKey: '',
icon: '',
formName: '',
groupName: '',
tableName: '',
permission: '',
remark: '',
status: '-1',
isActive: '1',
isAutoAudit: '0',
sort: 1,
});
// 定义校验规则
const dataRules = ref({
flowKey: [{required: true, message: '流程KEY不能为空', trigger: 'blur'}],
icon: [{required: true, message: '表单图标不能为空', trigger: 'blur'}],
formName: [{required: true, message: '表单名称不能为空', trigger: 'blur'}],
groupName: [{required: true, message: '分组名称不能为空', trigger: 'blur'}],
isActive: [{required: true, message: '默认展示不能为空', trigger: 'blur'}],
isAutoAudit: [{required: true, message: '提交时自动审批不能为空', trigger: 'blur'}],
sort: [{required: true, message: '排序值不能为空', trigger: 'blur'}],
type: [{required: true, message: '表单类型不能为空', trigger: 'blur'}],
status: [{required: true, message: '状态不能为空', trigger: 'blur'}],
})
// 打开弹窗
const openDialog = (type: string, id: string) => {
visible.value = true
operType.value = type;
setPropsNull(form, 'id', 'flowKey', 'defFlowId')
if (type === 'add') {
title.value = t('common.addBtn');
} else if (type === 'edit') {
title.value = t('common.editBtn');
} else if (type === 'view') {
title.value = t('common.viewBtn');
}
// 重置表单数据
nextTick(() => {
dataFormRef.value?.resetFields();
// 获取FlowApplication信息
if (id) {
form.id = id
getFlowApplicationData(id)
}
});
};
function handleAddGroupName() {
useMessageBox().prompt("请输入新的分组名称")
.then(({ value }) => {
form.groupName = value
onUpdate(dicData, form);
})
}
function changeSubTableName() {
form.subMainField = null
form.mainSubProp = null
}
function remoteMethod(query: string) {
remoteMethodByKey(query, onLoad, dicData, 'roleName', "permission")
}
// 提交
const onSubmit = async () => {
const valid = await dataFormRef.value.validate().catch(() => {});
if (!valid) return false;
if (form.type === DIC_PROP.FORM_TYPE[1].value) {
if (!form.path) {
notifyLeft('当前表单类型为系统表单【PC端路径】不能为空', 'warning')
return false
}
}
if(!validateNull(form.subTableName)) {
if (!form.subMainField || !form.mainSubProp) {
notifyLeft('请填写 关联主表列名 或者 关联子表属性', 'warning')
return false
}
}
let isDesign = form.type !== DIC_PROP.FORM_TYPE[1].value;
if (isDesign && !form.formInfo) {
notifyLeft('当前表单类型为设计表单,请先完善第二项设计的表单内容', 'warning')
return false
}
validateFormTypeSave(form)
try {
loading.value = true;
form.id ? await putObj(form) : await addObj(form);
useMessage().success(t(form.id ? 'common.editSuccessText' : 'common.addSuccessText'));
visible.value = false;
emit('refresh');
} catch (err: any) {
useMessage().error(err.msg);
} finally {
loading.value = false;
}
};
// 初始化表单数据
const getFlowApplicationData = (id: string) => {
// 获取数据
loading.value = true
getObj(id).then((res: any) => {
Object.assign(form, res.data)
onFormLoaded(dicData, form);
}).finally(() => {
loading.value = false
})
};
// 暴露变量
defineExpose({
openDialog
});
</script>

View File

@@ -0,0 +1,177 @@
<template>
<div>
<div class="header">
<el-tabs v-model="form.active" @tab-click="methods.activeMenu">
<el-tab-pane label="① 表单设置" name="formSetting"></el-tab-pane>
<el-tab-pane label="② 表单设计" name="formDesign"></el-tab-pane>
<el-tab-pane label="③ 流程设计" name="flowDesign"></el-tab-pane>
</el-tabs>
<div class="btn-publish" v-if="form.active !== 'flowDesign'">
<el-button icon="CaretRight" type="primary" @click="methods.activeMenu({paneName: form.active === 'formSetting' ? 'formDesign' : 'flowDesign'})">下一步
</el-button>
</div>
<div class="btn-publish" v-if="form.active === 'flowDesign'">
<template v-if="!validateRunFlow(props)">
<el-button icon="Upload" type="primary" @click="methods.publish(null, true)">升版本
</el-button>
<el-button icon="CirclePlus" type="primary" @click="methods.publish('-1')">暂存
</el-button>
</template>
<el-button icon="Promotion" type="primary" @click="methods.publish('1')">发布
</el-button>
</div>
<div class="btn-back">
<el-button @click="methods.exitFlowForm" icon="Back" circle></el-button>
<span style="margin-left: 20px;">
<i :class="props.currFlowForm.icon"></i>
<span>{{props.currFlowForm.formName }} V{{ props.currFlowForm.version }}
</span>
</span>
</div>
</div>
</div>
</template>
<script setup lang="ts" name="FlowDesignHeader">
import {useI18n} from "vue-i18n"
import {useMessageBox} from "/@/hooks/message";
import {notifyLeft} from "/@/flow";
import {validateNull} from "/@/utils/validate";
const {t} = useI18n();
const {proxy} = getCurrentInstance();
const $emit = defineEmits(["handleDesignFlow", "syncCurrFlowForm", "publish"]);
const props = defineProps({
currFlowForm: {
type: Object,
default: null,
}
});
const form = reactive({
active: "formSetting",
interval: {},
intervalTime: 2
});
const methods = {
publish(status, version) {
$emit('publish', form.active, () => {
props.currFlowForm[form.active] = true
}, status, version)
},
async exitFlowForm() {
let text = ''
// 判断是否都保存
if (props.currFlowForm.formSetting !== true) text += '【表单设置】,'
if (props.currFlowForm.formDesign !== true) text += '【表单设计】,'
if (props.currFlowForm.flowDesign !== true) text += '【流程设计】'
if (!text) {
$emit("handleDesignFlow", false);
return
}
try {
await useMessageBox().confirm(text + '未保存,是否继续退出?');
} catch {
return;
}
$emit("handleDesignFlow", false);
},
activeMenu(tab) {
let menu = tab.paneName
let active = form.active;
// 判断是否在操作中
if (props.currFlowForm.flowDesigning === true) {
if (!validateNull(form.interval)) clearInterval(form.interval);
let intervalTime = 0;
form.interval = setInterval(() => {
methods.validateFlowDesigning(menu, active, intervalTime)
intervalTime++;
}, 1000);
return true
}
methods.doActiveMenu(menu, active);
},
validateFlowDesigning(menu, active, intervalTime = 0){
notifyLeft('流程设计操作中,请稍后', 'warning')
if (intervalTime >= form.intervalTime) {
props.currFlowForm.flowDesigning = false
}
if (props.currFlowForm.flowDesigning === true) return
methods.doActiveMenu(menu, active)
clearInterval(form.interval);
},
doActiveMenu(menu, active){
$emit("syncCurrFlowForm", menu, active, (preSave)=>{
if (!preSave) menu = active
props.currFlowForm[menu] = false;
form.active = menu;
props.currFlowForm.active = menu;
});
},
// 关闭提示
listenPage() {
window.onbeforeunload = function (e) {
e = e || window.event;
if (e) {
e.returnValue = "关闭提示";
}
return "关闭提示";
};
}
}
onMounted(() => {
methods.listenPage();
});
</script>
<style lang="scss" scoped>
.header {
min-width: 980px;
.el-tabs {
position: absolute;
top: 15px;
z-index: 999;
display: flex;
justify-content: center;
width: 100%;
}
.btn-publish {
position: absolute;
top: 20px;
z-index: 1000;
right: 40px !important;
i {
margin-right: 6px;
}
button {
width: 74px;
height: 28px;
border-radius: 15px;
}
}
.btn-back {
position: absolute;
top: 20px;
z-index: 1000;
left: 20px !important;
font-size: 18px;
i {
margin-right: 6px;
}
button {
width: 44px;
height: 28px;
border-radius: 15px;
}
}
}
</style>

View File

@@ -0,0 +1,377 @@
<template>
<div>
<el-form ref="dataFormRef" :model="props.currFlowForm" :rules="dataRules" label-width="150px"
v-loading="loading"
label-position="left"
:disabled="operType === 'view'">
<el-row :gutter="24">
<el-col :span="12" class="mb20" :offset="6">
<el-form-item :label="t('flowApplication.flowKey')" prop="flowKey">
<el-input v-model="props.currFlowForm.flowKey" clearable
:placeholder="t('flowApplication.inputFlowKeyTip')"/>
</el-form-item>
</el-col>
<el-col :span="12" class="mb20" :offset="6">
<el-form-item :label="t('flowApplication.formName')" prop="formName">
<el-input v-model="props.currFlowForm.formName" maxlength="20" show-word-limit clearable
:placeholder="t('flowApplication.inputFormNameTip')"/>
</el-form-item>
</el-col>
<el-col :span="12" class="mb20" :offset="6">
<el-form-item :label="t('flowApplication.icon')" prop="icon">
<IconSelector :placeholder="$t('flowApplication.inputIconTip')"
v-model="props.currFlowForm.icon"/>
</el-form-item>
</el-col>
<el-col :span="12" class="mb20" :offset="6">
<el-form-item :label="t('flowApplication.groupName')" prop="groupName">
<el-select v-model="props.currFlowForm.groupName" style="width: 83%!important;"
:placeholder="t('flowApplication.inputGroupNameTip')"
clearable filterable allowCreate defaultFirstOption>
<el-option v-for="(item, index) in dicData.groupName" :key="index" :label="item.groupName"
:value="item.groupName"></el-option>
</el-select>
<el-button
type="primary" size="small" round style="margin-left: 10px"
@click="handleAddGroupName"
>新增分组
</el-button>
</el-form-item>
</el-col>
<el-col :span="12" class="mb20" :offset="6">
<el-form-item :label="t('flowApplication.type')" prop="type">
<el-select v-model="props.currFlowForm.type" :disabled="!!validateRunFlow(props)"
:placeholder="t('flowApplication.inputTypeTip')" clearable filterable>
<el-option v-for="(item, index) in DIC_PROP.FORM_TYPE" :key="index" :label="item.label" :value="item.value"></el-option>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12" class="mb20" :offset="6" v-if="props.currFlowForm.type === DIC_PROP.FORM_TYPE[1].value">
<el-tooltip content="当表单设计的页面为空时可配置本地自定义主表单Vue页面路径" placement="top">
<el-form-item :label="t('tabsOption.path')" prop="path">
<el-input v-model="props.currFlowForm.path" :placeholder="t('tabsOption.inputPathTip')" clearable>
</el-input>
</el-form-item>
</el-tooltip>
</el-col>
<el-col :span="12" class="mb20" :offset="6">
<el-form-item :label="t('flowApplication.tableName')" prop="tableName">
<el-tooltip content="若自行调用接口保存表单数据,则此处需为空" placement="top">
<el-select v-model="props.currFlowForm.tableName" @clear="props.currFlowForm.tableName = null"
:placeholder="t('flowApplication.inputTableNameTip')" style="width: 83%!important;"
clearable filterable>
<el-option v-for="(item, index) in dicData.tableName" :key="index" :label="item.tableName"
:value="item.tableName"></el-option>
</el-select>
</el-tooltip>
<el-button
type="primary" size="small" round style="margin-left: 10px"
@click="addUpdateTableName()"
>{{ props.currFlowForm.tableName ? '修改表' : '新增表'}}
</el-button>
</el-form-item>
</el-col>
<el-col :span="12" class="mb20" :offset="6" v-if="!validateRunFlow(props)">
<el-form-item :label="t('flowApplication.permission')" prop="permission">
<el-tooltip content="请输入角色名称进行模糊搜索,默认所有人都可以发起该流程" placement="top">
<el-select v-model="props.currFlowForm.permission" :placeholder="t('flowApplication.inputPermissionTip')"
clearable filterable multiple
remote :remote-method="remoteMethod" :reserve-keyword="false">
<el-option v-for="(item, index) in dicData.permission" :key="index" :label="item.roleName"
:value="item.roleId"></el-option>
</el-select>
</el-tooltip>
</el-form-item>
</el-col>
<el-col :span="12" class="mb20" :offset="6">
<el-form-item :label="t('flowApplication.subTableName')" prop="subTableName">
<el-tooltip content="当存在多个关联子表时,多个子表名称名顺序与《关联子表属性》顺序一一对应" placement="top">
<el-select v-model="props.currFlowForm.subTableName" :placeholder="t('flowApplication.inputSubTableNameTip')" style="width: 83%!important;"
@change="changeSubTableName" clearable filterable multiple>
<el-option v-for="(item, index) in dicData.tableName" :key="index"
:label="item.tableName" :value="item.tableName">
<span> {{ item.tableName }} </span>
<span style="float: right; color: #409EFF" @click="addUpdateTableName(item.tableName, true)"> 点击修改 </span>
</el-option>
</el-select>
</el-tooltip>
<el-button
type="primary" size="small" round style="margin-left: 10px"
@click="addUpdateTableName(null, true)"
>新增子表
</el-button>
</el-form-item>
</el-col>
<el-col :span="12" class="mb20" :offset="6" v-if="!validateNull(props.currFlowForm.subTableName)">
<el-tooltip content="当关联子表名称存在时可配置子表中关联【主表主键】的列名如main_id" placement="top">
<el-form-item :label="t('flowApplication.subMainField')" prop="subMainField">
<el-input v-model="props.currFlowForm.subMainField" :placeholder="t('flowApplication.inputSubMainFieldTip')"></el-input>
</el-form-item>
</el-tooltip>
</el-col>
<el-col :span="12" class="mb20" :offset="6" v-if="!validateNull(props.currFlowForm.subTableName)">
<el-tooltip content="当关联子表名称存在时可配置主表中关联【子表集合数据】的属性名如subFormList。多个子表属性名以逗号分割注意与《关联子表名称》顺序一一对应" placement="top">
<el-form-item :label="t('flowApplication.mainSubProp')" prop="mainSubProp">
<el-input v-model="props.currFlowForm.mainSubProp" :placeholder="t('flowApplication.inputMainSubPropTip')"></el-input>
</el-form-item>
</el-tooltip>
</el-col>
<el-col :span="12" class="mb20" :offset="6">
<el-form-item :label="t('flowApplication.remark')" prop="remark">
<el-input v-model="props.currFlowForm.remark" type="textarea" :placeholder="t('flowApplication.inputRemarkTip')"/>
</el-form-item>
</el-col>
<el-col :span="12" class="mb20" :offset="6">
<el-form-item :label="t('flowApplication.sort')" prop="sort">
<el-input-number :min="1" :max="1000" v-model="props.currFlowForm.sort"
:placeholder="t('flowApplication.inputSortTip')"></el-input-number>
</el-form-item>
</el-col>
<el-col :span="12" class="mb20" v-if="!validateRunFlow(props)" :offset="6">
<el-tooltip content="在审批时默认展示的第1个页面。当存在多个默认展示页面时优先展示排序值最小的" placement="top">
<el-form-item :label="t('tabsOption.isActive')" prop="isActive">
<el-radio-group v-model="props.currFlowForm.isActive">
<el-radio v-for="(item, index) in DIC_PROP.YES_OR_NO" :key="index" :label="item.value" >
{{ getLabelByLanguage(item) }}
</el-radio>
</el-radio-group>
</el-form-item>
</el-tooltip>
</el-col>
<el-col :span="12" class="mb20" v-if="!validateRunFlow(props)" :offset="6">
<el-tooltip content="在审批时不会显示审批按钮,在页面点提交按钮会自动流转到下一步" placement="top">
<el-form-item :label="t('tabsOption.isAutoAudit')" prop="isAutoAudit">
<el-radio-group v-model="props.currFlowForm.isAutoAudit">
<el-radio v-for="(item, index) in DIC_PROP.YES_OR_NO" :key="index" :label="item.value" >
{{ getLabelByLanguage(item) }}
</el-radio>
</el-radio-group>
</el-form-item>
</el-tooltip>
</el-col>
<el-col :span="12" class="mb20" :offset="6">
<el-form-item :label="t('flowApplication.status')" prop="status">
<el-radio-group v-model="props.currFlowForm.status">
<el-radio v-for="(item, index) in DIC_PROP.TEMP_STATUS" :key="index" :label="item.value">
{{ getLabelByLanguage(item) }}
</el-radio>
</el-radio-group>
</el-form-item>
</el-col>
</el-row>
</el-form>
<!-- 编辑新增 -->
<create-table ref="createTableRef" @refreshDone="createTableDone" />
</div>
</template>
<script setup lang="ts" name="FlowApplicationForm">
import {useMessage, useMessageBox} from "/@/hooks/message";
import * as runApplication from "/@/api/order/run-application";
import * as flowApplication from "/@/api/order/flow-application";
import other from '/@/utils/other';
import {notifyLeft, stringifyWithFunctions, validateRunFlow} from "/@/flow";
import {useI18n} from "vue-i18n"
import {
onFormLoadedUrl,
onLoadDicUrl,
onUpdateDicData,
remoteMethodByKey
} from "/@/flow/components/convert-name/convert";
import {validateNull} from "/@/utils/validate";
import {DIC_PROP} from "../../support/dict-prop";
import {validateFormTypeSave, vueKey} from "/@/api/order/order-key-vue";
import {PROP_CONST} from "../../support/prop-const";
const {t} = useI18n();
const {proxy} = getCurrentInstance();
const props = defineProps({
currFlowForm: {
type: Object,
default: null,
}
});
// 引入组件
const IconSelector = defineAsyncComponent(() => import('/@/components/IconSelector/index.vue'));
const CreateTable = defineAsyncComponent(() => import('/@/views/order/create-table/form.vue'));
// 定义变量内容
const dataFormRef = ref();
const loading = ref(false);
const operType = ref(false);
// 定义字典
const dicData = reactive({});
const onLoad = onLoadDicUrl({key: "groupName"}, {key: "tableName"});
const onFormLoaded = onFormLoadedUrl({key: "permission"});
const onUpdate = onUpdateDicData({key: "groupName"});
onMounted(async () => {
await onLoad(dicData);
await onFormLoaded(dicData, props.currFlowForm);
let isNoGroupName = !props.currFlowForm.groupName && dicData.groupName.length > 0;
if (isNoGroupName) {
props.currFlowForm.groupName = dicData.groupName[0].groupName
}
});
const data = reactive({subTableName: null, updateSubTableName: null, isSubTableName: false});
// 定义校验规则
const dataRules = ref({
flowKey: [{required: true, message: '流程KEY不能为空', trigger: 'blur'}],
icon: [{required: true, message: '表单图标不能为空', trigger: 'blur'}],
type: [{required: true, message: '表单类型不能为空', trigger: 'blur'}],
formName: [{required: true, message: '表单名称不能为空', trigger: 'blur'}],
groupName: [{required: true, message: '分组名称不能为空', trigger: 'blur'}],
status: [{required: true, message: '状态不能为空', trigger: 'blur'}],
})
function handleAddGroupName() {
useMessageBox().prompt("请输入新的分组名称")
.then(({ value }) => {
props.currFlowForm.groupName = value
onUpdate(dicData, props.currFlowForm);
})
}
function changeSubTableName() {
if (data.isSubTableName) {
if (data.subTableName) props.currFlowForm.subTableName = data.subTableName
return
}
if (!validateNull(props.currFlowForm.subTableName)) {
return
}
props.currFlowForm.subMainField = null
props.currFlowForm.mainSubProp = null
}
function addUpdateTableName(subTableName?, isSubTableName?) {
data.isSubTableName = isSubTableName
if (isSubTableName && subTableName) {
data.subTableName = props.currFlowForm.subTableName
data.updateSubTableName = subTableName
}
let tableName = isSubTableName ? subTableName : props.currFlowForm.tableName;
if (tableName) {
if (tableName === PROP_CONST.COMMON.tableName) {
notifyLeft('当前表为系统表,请勿修改', 'warning')
} else {
let find = dicData.tableName.find(f => f.tableName === tableName);
proxy.$refs.createTableRef.openDialog('edit', find.id)
}
} else {
proxy.$refs.createTableRef.openDialog('add')
}
}
function createTableDone(tableName, operType, isOnSubmit) {
if (data.isSubTableName) {
if (isOnSubmit && operType === 'edit' && data.updateSubTableName !== tableName){
let includes = props.currFlowForm.subTableName.includes(data.updateSubTableName);
if (includes) {
// 修改表名
let findIndex = props.currFlowForm.subTableName.findIndex(f => f === data.updateSubTableName);
props.currFlowForm.subTableName.splice(findIndex, 1, tableName)
}
} else if (isOnSubmit && operType === 'add'){
props.currFlowForm.subTableName.push(tableName)
}
data.isSubTableName = false
data.updateSubTableName = null
data.subTableName = null
} else if (isOnSubmit){
props.currFlowForm.tableName = tableName
}
if (!isOnSubmit) return
const onLoad = onLoadDicUrl({key: "tableName"});
onLoad(dicData)
}
async function validatePreSave() {
const valid = await dataFormRef.value.validate().catch(() => {});
if (!valid) {
notifyLeft('请先完善表单设置内容', 'warning')
return false;
}
let isSys = props.currFlowForm.type === DIC_PROP.FORM_TYPE[1].value;
if (isSys && !props.currFlowForm.path) {
notifyLeft('当前表单类型为系统表单【PC端路径】不能为空', 'warning')
return false
}
if(!validateNull(props.currFlowForm.subTableName)) {
if (!props.currFlowForm.subMainField || !props.currFlowForm.mainSubProp) {
notifyLeft('请填写 关联主表列名 或者 关联子表属性', 'warning')
return false
}
}
return true
}
function validateOtherSave() {
let isDesign = props.currFlowForm.type !== DIC_PROP.FORM_TYPE[1].value;
if (isDesign && !props.currFlowForm.formInfo) {
notifyLeft('当前表单类型为设计表单,请先完善第二项设计的表单内容', 'warning')
return false
}
return true
}
function remoteMethod(query: string) {
remoteMethodByKey(query, onLoad, dicData, 'roleName', "permission")
}
const handleSubmit = async (callback, status) => {
if (status) {
props.currFlowForm.status = status
}
let preSave = await validatePreSave();
if (!preSave) return
if (!validateOtherSave()) return
let formJson = other.deepClone(props.currFlowForm)
formJson.formInfo = stringifyWithFunctions(formJson.formInfo)
validateFormTypeSave(formJson)
try {
loading.value = true;
// 判断是否为低代码
if (validateRunFlow(props) && props.currFlowForm.tableName) {
await runApplication.putObjNoStatus(formJson)
} else {
await flowApplication.addObj(formJson);
}
props.currFlowForm.formSetting = true
notifyLeft('当前表单设置保存成功')
if (callback) callback()
} catch (err: any) {
useMessage().error(err);
} finally {
loading.value = false;
}
}
// 暴露变量
defineExpose({
handleSubmit, validatePreSave, validateOtherSave
});
</script>

View File

@@ -0,0 +1,53 @@
<template>
<div>
<sign ref="signRef"
v-model:bg-color="bgColor"
:width="800"
:height="150"
:is-crop="false"
:isClearBgColor="false"
:line-width="3"
:line-color="'#000000'"
></sign>
</div>
<div style="margin-left: 15px">
<el-link type="primary" @click.stop="handleReset">{{ t('jfcomment.reSign') }}</el-link>
</div>
</template>
<script lang="ts" setup name="SignName">
import {useMessage} from "/@/hooks/message";
import {useI18n} from "vue-i18n";
const {t, locale} = useI18n();
const $message = useMessage();
const {proxy} = getCurrentInstance();
const Sign = defineAsyncComponent(() => import('./sign.vue'));
const bgColor = ref('#F6F8FA');
let props = defineProps({
currJob: {
type: Object,
default: {},
},
});
const handleReset = () => {
proxy.$refs.signRef.reset();
props.currJob.signName = null
};
const handleGenerate = async () => {
await proxy.$refs.signRef.generate()
.then((res: any) => {
props.currJob.signName = res;
})
.catch(() => {
});
};
defineExpose({ handleGenerate });
</script>
<style scoped lang="less">
</style>

View File

@@ -0,0 +1,283 @@
<template>
<canvas
id="canvas"
@mousedown="mouseDown"
@mousemove="mouseMove"
@mouseup="mouseUp"
@touchstart="touchStart"
@touchmove="touchMove"
@touchend="touchEnd"
></canvas>
</template>
<script lang="ts" setup>
const props = defineProps({
width: {
type: Number,
default: 800,
},
height: {
type: Number,
default: 300,
},
lineWidth: {
type: Number,
default: 4,
},
lineColor: {
type: String,
default: '#000000',
},
bgColor: {
type: String,
default: '',
},
isCrop: {
type: Boolean,
default: false,
},
isClearBgColor: {
type: Boolean,
default: true,
},
});
let canvas: any = null; //document.getElementById('canvas')
let hasDrew = false;
let resultImg = '';
let points: any = [];
let canvasTxt: any = null;
let startX = 0;
let startY = 0;
let isDrawing = false;
let sratio: any = 1;
onBeforeMount(() => {
window.addEventListener('resize', resizeHandler);
});
onUnmounted(() => {
window.removeEventListener('resize', resizeHandler);
});
onMounted(() => {
// const canvas = this.$refs.canvas
canvas = document.getElementById('canvas');
canvas.height = props.height;
canvas.width = props.width;
canvas.style.background = myBg.value;
resizeHandler();
// 在画板以外松开鼠标后冻结画笔
document.onmouseup = () => {
isDrawing = false;
};
});
const ratio: any = computed(() => {
return props.height / props.width;
});
const stageInfo: any = computed(() => {
return canvas.getBoundingClientRect();
});
const myBg = computed(() => {
return props.bgColor ? props.bgColor : 'rgba(255, 255, 255, 0)';
});
watch(myBg, (newVal) => {
canvas.style.background = newVal;
});
const resizeHandler = () => {
canvas.style.width = props.width + 'px';
const realw = parseFloat(window.getComputedStyle(canvas).width);
canvas.style.height = ratio.value * realw + 'px';
canvasTxt = canvas.getContext('2d');
canvasTxt.scale(1 * sratio, 1 * sratio);
sratio = realw / props.width;
canvasTxt.scale(1 / sratio, 1 / sratio);
};
// pc
const mouseDown = (e) => {
e = e || event;
e.preventDefault();
isDrawing = true;
hasDrew = true;
let obj = {
x: e.offsetX,
y: e.offsetY,
};
drawStart(obj);
};
const mouseMove = (e) => {
e = e || event;
e.preventDefault();
if (isDrawing) {
let obj = {
x: e.offsetX,
y: e.offsetY,
};
drawMove(obj);
}
};
const mouseUp = (e) => {
e = e || event;
e.preventDefault();
let obj = {
x: e.offsetX,
y: e.offsetY,
};
drawEnd(obj);
isDrawing = false;
};
// mobile
const touchStart = (e) => {
e = e || event;
e.preventDefault();
hasDrew = true;
if (e.touches.length === 1) {
let obj = {
x: e.targetTouches[0].clientX - canvas.getBoundingClientRect().left,
y: e.targetTouches[0].clientY - canvas.getBoundingClientRect().top,
};
drawStart(obj);
}
};
const touchMove = (e) => {
e = e || event;
e.preventDefault();
if (e.touches.length === 1) {
let obj = {
x: e.targetTouches[0].clientX - canvas.getBoundingClientRect().left,
y: e.targetTouches[0].clientY - canvas.getBoundingClientRect().top,
};
drawMove(obj);
}
};
const touchEnd = (e) => {
e = e || event;
e.preventDefault();
if (e.touches.length === 1) {
let obj = {
x: e.targetTouches[0].clientX - canvas.getBoundingClientRect().left,
y: e.targetTouches[0].clientY - canvas.getBoundingClientRect().top,
};
drawEnd(obj);
}
};
// 绘制
const drawStart = (obj) => {
startX = obj.x;
startY = obj.y;
canvasTxt.beginPath();
canvasTxt.moveTo(startX, startY);
canvasTxt.lineTo(obj.x, obj.y);
canvasTxt.lineCap = 'round';
canvasTxt.lineJoin = 'round';
canvasTxt.lineWidth = props.lineWidth * sratio;
canvasTxt.stroke();
canvasTxt.closePath();
points.push(obj);
};
const drawMove = (obj) => {
canvasTxt.beginPath();
canvasTxt.moveTo(startX, startY);
canvasTxt.lineTo(obj.x, obj.y);
canvasTxt.strokeStyle = props.lineColor;
canvasTxt.lineWidth = props.lineWidth * sratio;
canvasTxt.lineCap = 'round';
canvasTxt.lineJoin = 'round';
canvasTxt.stroke();
canvasTxt.closePath();
startY = obj.y;
startX = obj.x;
points.push(obj);
};
const drawEnd = (obj) => {
canvasTxt.beginPath();
canvasTxt.moveTo(startX, startY);
canvasTxt.lineCap = 'round';
canvasTxt.lineJoin = 'round';
canvasTxt.stroke();
canvasTxt.closePath();
points.push(obj);
points.push({ x: -1, y: -1 });
};
// 操作
const generate = () => {
const pm = new Promise((resolve, reject) => {
if (!hasDrew) {
reject(`Warning: Not Signned!`);
return;
}
var resImgData = canvasTxt.getImageData(0, 0, canvas.width, canvas.height);
canvasTxt.globalCompositeOperation = 'destination-over';
canvasTxt.fillStyle = myBg.value;
canvasTxt.fillRect(0, 0, canvas.width, canvas.height);
resultImg = canvas.toDataURL();
var resultImg = resultImg;
canvasTxt.clearRect(0, 0, canvas.width, canvas.height);
canvasTxt.putImageData(resImgData, 0, 0);
canvasTxt.globalCompositeOperation = 'source-over';
if (props.isCrop) {
const crop_area = getCropArea(resImgData.data);
var crop_canvas: any = document.createElement('canvas');
const crop_ctx: any = crop_canvas.getContext('2d');
crop_canvas.width = crop_area[2] - crop_area[0];
crop_canvas.height = crop_area[3] - crop_area[1];
const crop_imgData = canvasTxt.getImageData(...crop_area);
crop_ctx.globalCompositeOperation = 'destination-over';
crop_ctx.putImageData(crop_imgData, 0, 0);
crop_ctx.fillStyle = myBg.value;
crop_ctx.fillRect(0, 0, crop_canvas.width, crop_canvas.height);
resultImg = crop_canvas.toDataURL();
crop_canvas = null;
}
resolve(resultImg);
});
return pm;
};
const emits = defineEmits(['update:bgColor']);
const reset = () => {
canvasTxt.clearRect(0, 0, canvas.width, canvas.height);
if (props.isClearBgColor) {
// this.$emit('update:bgColor', '')
emits('update:bgColor', '');
canvas.style.background = 'rgba(255, 255, 255, 0)';
}
points = [];
hasDrew = false;
resultImg = '';
};
const getCropArea = (imgData) => {
var topX = canvas.width;
var btmX = 0;
var topY = canvas.height;
var btnY = 0;
for (var i = 0; i < canvas.width; i++) {
for (var j = 0; j < canvas.height; j++) {
var pos = (i + canvas.width * j) * 4;
if (imgData[pos] > 0 || imgData[pos + 1] > 0 || imgData[pos + 2] || imgData[pos + 3] > 0) {
btnY = Math.max(j, btnY);
btmX = Math.max(i, btmX);
topY = Math.min(j, topY);
topX = Math.min(i, topX);
}
}
}
topX++;
btmX++;
topY++;
btnY++;
const data = [topX, topY, btmX, btnY];
return data;
};
defineExpose({ reset, generate });
</script>
<style scoped>
canvas {
max-width: 100%;
display: block;
}
</style>

View File

@@ -0,0 +1,322 @@
<template>
<div class="sign-wrapper">
<div v-show="!modelValue" class="sign-container">
<canvas
ref="canvasRef"
@mousedown="handleMouseDown"
@mousemove="handleMouseMove"
@mouseup="handleMouseUp"
@touchstart="handleTouchStart"
@touchmove="handleTouchMove"
@touchend="handleTouchEnd"
></canvas>
</div>
<div v-if="!props.disabled && !modelValue" class="sign-controls">
<el-space>
<el-button type="primary" @click="handleGenerate" size="small">
<el-icon><Check /></el-icon>
确认
</el-button>
<el-button @click="handleReset" size="small">
<el-icon><Refresh /></el-icon>
清空
</el-button>
<div>
<el-color-picker v-model="currentLineColor" size="small" />
</div>
</el-space>
</div>
<div v-if="modelValue" class="flex flex-col items-center sign-preview">
<el-image :src="modelValue" fit="contain" />
<div class="mt-2" v-if="!props.disabled">
<el-button @click="handleReset" size="small">
<el-icon><Refresh /></el-icon>
重签
</el-button>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { ref, onMounted, onBeforeUnmount, computed, watch } from 'vue';
import { Check, Refresh } from '@element-plus/icons-vue';
import { ElMessage } from 'element-plus';
import type { Point } from './types';
interface Props {
width?: number;
height?: number;
lineWidth?: number;
lineColor?: string;
bgColor?: string;
isCrop?: boolean;
isClearBgColor?: boolean;
modelValue?: string;
disabled?: boolean;
}
const props = defineProps({
width: {
type: Number,
default: 300,
},
height: {
type: Number,
default: 150,
},
lineWidth: {
type: Number,
default: 2,
},
lineColor: {
type: String,
default: '#000000',
},
bgColor: {
type: String,
default: '',
},
isCrop: {
type: Boolean,
default: false,
},
isClearBgColor: {
type: Boolean,
default: true,
},
modelValue: {
type: String,
default: null,
},
disabled: {
type: Boolean,
default: false,
},
});
const emit = defineEmits(['update:modelValue']);
const canvasRef = ref<HTMLCanvasElement | null>(null);
const ctx = ref<CanvasRenderingContext2D | null>(null);
const isDrawing = ref(false);
const hasDrew = ref(false);
const currentLineColor = ref(props.lineColor);
const points = ref<Point[]>([]);
const startPoint = ref<Point>({ x: 0, y: 0 });
const scaleRatio = ref(1);
const backgroundColor = computed(() => props.bgColor || 'rgba(255, 255, 255, 0)');
function initCanvas(): void {
const canvas = canvasRef.value;
if (!canvas) return;
const container = canvas.parentElement;
if (!container) return;
// Set fixed dimensions
canvas.width = props.width;
canvas.height = props.height;
// Set display dimensions
canvas.style.width = `${props.width}px`;
canvas.style.height = `${props.height}px`;
canvas.style.background = backgroundColor.value;
ctx.value = canvas.getContext('2d');
if (ctx.value) {
ctx.value.strokeStyle = currentLineColor.value;
ctx.value.lineWidth = props.lineWidth;
ctx.value.lineCap = 'round';
ctx.value.lineJoin = 'round';
}
}
function handleResize(): void {
const canvas = canvasRef.value;
if (!canvas || !ctx.value) return;
// Keep the same dimensions
canvas.width = props.width;
canvas.height = props.height;
ctx.value = canvas.getContext('2d');
if (!ctx.value) return;
ctx.value.strokeStyle = currentLineColor.value;
ctx.value.lineWidth = props.lineWidth;
ctx.value.lineCap = 'round';
ctx.value.lineJoin = 'round';
}
function drawPoint(point: Point): void {
if (!ctx.value) return;
ctx.value.beginPath();
ctx.value.moveTo(startPoint.value.x, startPoint.value.y);
ctx.value.lineTo(point.x, point.y);
ctx.value.strokeStyle = currentLineColor.value;
ctx.value.lineWidth = props.lineWidth * scaleRatio.value;
ctx.value.lineCap = 'round';
ctx.value.lineJoin = 'round';
ctx.value.stroke();
ctx.value.closePath();
startPoint.value = point;
points.value.push(point);
}
// Event handlers
function handleMouseDown(e: MouseEvent): void {
if (props.disabled) return;
e.preventDefault();
isDrawing.value = true;
hasDrew.value = true;
const point = {
x: e.offsetX,
y: e.offsetY,
};
startPoint.value = point;
points.value.push(point);
}
function handleMouseMove(e: MouseEvent): void {
if (!isDrawing.value || props.disabled) return;
e.preventDefault();
drawPoint({
x: e.offsetX,
y: e.offsetY,
});
}
function handleMouseUp(e: MouseEvent): void {
if (props.disabled) return;
e.preventDefault();
isDrawing.value = false;
points.value.push({ x: -1, y: -1 }); // Mark end of stroke
}
// Touch events
function handleTouchStart(e: TouchEvent): void {
if (props.disabled || !canvasRef.value) return;
e.preventDefault();
hasDrew.value = true;
const touch = e.touches[0];
const rect = canvasRef.value.getBoundingClientRect();
const point = {
x: touch.clientX - rect.left,
y: touch.clientY - rect.top,
};
startPoint.value = point;
points.value.push(point);
}
function handleTouchMove(e: TouchEvent): void {
if (props.disabled || !canvasRef.value) return;
e.preventDefault();
const touch = e.touches[0];
const rect = canvasRef.value.getBoundingClientRect();
drawPoint({
x: touch.clientX - rect.left,
y: touch.clientY - rect.top,
});
}
function handleTouchEnd(e: TouchEvent): void {
if (props.disabled) return;
e.preventDefault();
points.value.push({ x: -1, y: -1 }); // Mark end of stroke
}
// Actions
async function handleGenerate(): Promise<void> {
try {
const result = await generate();
emit('update:modelValue', result);
} catch (error) {
ElMessage.warning('请先进行签名');
}
}
function handleReset(): void {
reset();
emit('update:modelValue', '');
}
function generate(): Promise<string> {
return new Promise((resolve, reject) => {
if (!hasDrew.value || !canvasRef.value || !ctx.value) {
reject('请先进行签名');
return;
}
const canvas = canvasRef.value;
const context = ctx.value;
// Save current drawing
const imageData = context.getImageData(0, 0, canvas.width, canvas.height);
// Add background
context.globalCompositeOperation = 'destination-over';
context.fillStyle = backgroundColor.value;
context.fillRect(0, 0, canvas.width, canvas.height);
// Get result
const result = canvas.toDataURL();
// Restore original drawing
context.clearRect(0, 0, canvas.width, canvas.height);
context.putImageData(imageData, 0, 0);
context.globalCompositeOperation = 'source-over';
resolve(result);
});
}
function reset(): void {
if (!ctx.value || !canvasRef.value) return;
ctx.value.clearRect(0, 0, canvasRef.value.width, canvasRef.value.height);
points.value = [];
hasDrew.value = false;
if (props.isClearBgColor) {
canvasRef.value.style.background = 'rgba(255, 255, 255, 0)';
}
}
// Lifecycle
onMounted(() => {
initCanvas();
window.addEventListener('resize', handleResize);
document.addEventListener('mouseup', () => (isDrawing.value = false));
});
onBeforeUnmount(() => {
window.removeEventListener('resize', handleResize);
document.removeEventListener('mouseup', () => (isDrawing.value = false));
});
// Watch
watch(
() => backgroundColor.value,
(newVal) => {
if (canvasRef.value) {
canvasRef.value.style.background = newVal;
}
}
);
defineExpose({ reset, generate });
</script>

View File

@@ -0,0 +1,21 @@
export interface SignProps {
width?: number;
height?: number;
lineWidth?: number;
lineColor?: string;
bgColor?: string;
isCrop?: boolean;
isClearBgColor?: boolean;
modelValue?: string;
disabled?: boolean;
}
export interface Point {
x: number;
y: number;
}
export interface SignInstance {
reset: () => void;
generate: () => Promise<string>;
}

View File

@@ -0,0 +1,43 @@
.flow-header-drawer {
/*抽屉内容靠上*/
.el-drawer__header {
margin-bottom: 0;
}
}
.tinymce-print-drawer {
/*抽屉内容靠上*/
.el-drawer__header {
margin-bottom: 0;
}
}
.flow-overflow-drawer {
/*抽屉内容靠上*/
.el-drawer__header {
margin-bottom: 0;
}
/*隐藏滚动条*/
.el-drawer__body {
overflow: hidden;
}
}
.flow-dialog-drawer {
/*隐藏滚动条*/
.el-dialog__body {
overflow: hidden;
}
}
.flow-attr-drawer {
/*抽屉内容靠上*/
.el-drawer__header {
margin-bottom: 0;
margin-left: 15px;
}
.el-tabs__header .is-top{
//display: none;
}
}

View File

@@ -0,0 +1,109 @@
<template>
<el-container>
<el-header style="background: white">
<tabs-design-header :currFlowForm="props.currFlowForm" @handleDesignTabs="handleDesignTabs"
@syncCurrFlowForm="syncCurrFlowForm"
@publish="publishTabs"></tabs-design-header>
</el-header>
<div id="initClientHeight" style="min-width: 980px;">
<tabs-setting :currFlowForm="props.currFlowForm" ref="tabsSetting" v-show="props.currFlowForm.active === 'tabsSetting'"/>
<form-design :currFlowForm="props.currFlowForm" ref="formDesign" v-show="showFormDesign()"/>
<form-def-perm :currFlowForm="props.currFlowForm" ref="formDefPerm" v-show="showFormDefPerm()"/>
</div>
</el-container>
</template>
<script setup lang="ts" name="TabsFormDesign">
import {useI18n} from "vue-i18n"
import {formWidgetDesignHeight} from "/@/flow/utils";
import {DIC_PROP} from "../../support/dict-prop";
const {t} = useI18n();
const { proxy } = getCurrentInstance();
// 引入组件
const TabsDesignHeader = defineAsyncComponent(() => import('./header.vue'));
const FormDesign = defineAsyncComponent(() => import('../form-create/designer.vue'));
const TabsSetting = defineAsyncComponent(() => import('./setting.vue'));
const FormDefPerm = defineAsyncComponent(() => import('/@/views/jsonflow/form-option/form-def-perm.vue'));
const $emit = defineEmits(['handleDesignTabs']);
const props = defineProps({
currFlowForm: {
type: Object,
default: null,
}
});
function initClientHeight() {
nextTick(() => {
let browserHeight = window.innerHeight || document.documentElement.clientHeight || document.body.clientHeight;
formWidgetDesignHeight(browserHeight);
})
}
function handleDesignTabs(bool: boolean) {
$emit("handleDesignTabs", bool);
}
function showFormDesign() {
return props.currFlowForm.active === 'formDesign' && props.currFlowForm.type !== DIC_PROP.FORM_TYPE[1].value
}
function showFormDefPerm() {
return props.currFlowForm.active === 'formDesign' && props.currFlowForm.type === DIC_PROP.FORM_TYPE[1].value
}
async function syncCurrFlowForm(callback) {
let preSave = await proxy.$refs.tabsSetting.validatePreSave();
callback(preSave)
}
async function syncCurrFormInfo(callback?) {
if (props.currFlowForm.type !== DIC_PROP.FORM_TYPE[1].value) {
proxy.$refs.formDefPerm.syncCurrFormInfo(true)
// 保存表单信息
return await proxy.$refs.formDesign.handleSubmit(() => {
if (callback) callback()
}, true);
} else {
proxy.$refs.formDefPerm.syncCurrFormInfo(false)
props.currFlowForm.formInfo = null
return true
}
}
async function publishTabs(menu, callback, status) {
// 保存表单信息
let preSave = await syncCurrFormInfo(callback)
if (!preSave) return
// 校验表单设计
let otherSave = await proxy.$refs.tabsSetting.validateOtherSave();
if (!otherSave) return
// 保存字段信息
if (props.currFlowForm.type === DIC_PROP.FORM_TYPE[1].value) {
await proxy.$refs.formDefPerm.handleSubmit(() => {
if (callback) callback()
});
}
// 保存页面配置
proxy.$refs.tabsSetting.handleSubmit(() => {
if (callback) callback()
}, status)
}
// 监听双向绑定
watch(
() => props.currFlowForm.id,
(val) => {
initClientHeight()
}
);
onMounted(() => {
initClientHeight();
});
</script>
<style lang="scss" scoped>
</style>

View File

@@ -0,0 +1,244 @@
<template>
<el-dialog :title="title" v-model="visible" width="80%"
:close-on-click-modal="false" draggable>
<el-form ref="dataFormRef" :model="form" :rules="dataRules" label-width="130px" v-loading="loading" :disabled="operType === 'view'">
<el-row :gutter="24">
<el-col :span="12" class="mb20">
<el-form-item :label="t('tabsOption.formName')" prop="formName">
<el-input v-model="form.formName" :placeholder="t('tabsOption.inputFormNameTip')" maxlength="20" show-word-limit clearable/>
</el-form-item>
</el-col>
<el-col :span="12" class="mb20">
<el-form-item :label="t('tabsOption.icon')" prop="icon">
<IconSelector :placeholder="$t('tabsOption.inputIconTip')" v-model="form.icon" />
</el-form-item>
</el-col>
<el-col :span="12" class="mb20">
<el-form-item :label="t('tabsOption.groupName')" prop="groupName">
<el-select v-model="form.groupName" :placeholder="t('tabsOption.inputGroupNameTip')" style="width: 80%!important;"
clearable filterable allowCreate defaultFirstOption>
<el-option v-for="(item, index) in dicData.groupName" :key="index" :label="item.groupName" :value="item.groupName"></el-option>
</el-select>
<el-button
type="primary" size="small" round style="margin-left: 10px"
@click="handleAddGroupName"
>新增分组
</el-button>
</el-form-item>
</el-col>
<el-col :span="12" class="mb20">
<el-form-item :label="t('tabsOption.type')" prop="type">
<el-select v-model="form.type" :placeholder="t('tabsOption.inputTypeTip')" clearable filterable>
<el-option v-for="(item, index) in DIC_PROP.FORM_TYPE" :key="index" :label="item.label" :value="item.value"></el-option>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12" class="mb20" v-if="form.type === DIC_PROP.FORM_TYPE[1].value">
<el-form-item :label="t('tabsOption.path')" prop="path">
<el-tooltip content="当表单类型为系统表单时可输入本地Vue组件路径或者外部系统http或https开头的页面路径" placement="top">
<el-input v-model="form.path" :placeholder="t('tabsOption.inputPathTip')" clearable>
</el-input>
</el-tooltip>
</el-form-item>
</el-col>
<el-col :span="12" class="mb20">
<el-form-item :label="t('flowApplication.remark')" prop="remark">
<el-input v-model="form.remark" type="textarea" :placeholder="t('flowApplication.inputRemarkTip')"/>
</el-form-item>
</el-col>
<el-col :span="12" class="mb20">
<el-form-item :label="t('tabsOption.sort')" prop="sort">
<el-input-number :min="1" :max="1000" v-model="form.sort" :placeholder="t('tabsOption.inputSortTip')"></el-input-number>
</el-form-item>
</el-col>
<el-col :span="12" class="mb20">
<el-form-item :label="t('tabsOption.version')" prop="version">
<el-input-number :min="1" :max="1000" v-model="form.version" :placeholder="t('tabsOption.inputVersionTip')"></el-input-number>
</el-form-item>
</el-col>
<el-col :span="12" class="mb20">
<el-form-item :label="t('tabsOption.isActive')" prop="isActive">
<el-radio-group v-model="form.isActive">
<el-radio v-for="(item, index) in DIC_PROP.YES_OR_NO" :key="index" :label="item.value" >
{{ getLabelByLanguage(item) }}
</el-radio>
</el-radio-group>
</el-form-item>
</el-col>
<el-col :span="12" class="mb20">
<el-form-item :label="t('tabsOption.isAutoAudit')" prop="isAutoAudit">
<el-radio-group v-model="form.isAutoAudit">
<el-radio v-for="(item, index) in DIC_PROP.YES_OR_NO" :key="index" :label="item.value" >
{{ getLabelByLanguage(item) }}
</el-radio>
</el-radio-group>
</el-form-item>
</el-col>
<el-col :span="12" class="mb20">
<el-form-item :label="t('tabsOption.status')" prop="status">
<el-radio-group v-model="form.status">
<el-radio v-for="(item, index) in DIC_PROP.TEMP_STATUS" :key="index" :label="item.value" >
{{ getLabelByLanguage(item) }}
</el-radio>
</el-radio-group>
</el-form-item>
</el-col>
</el-row>
</el-form>
<template #footer v-if="operType !== 'view'">
<span class="dialog-footer">
<el-button @click="visible = false">{{ $t('common.cancelButtonText') }}</el-button>
<el-button type="primary" @click="onSubmit" :disabled="loading">{{ $t('common.confirmButtonText') }}</el-button>
</span>
</template>
</el-dialog>
</template>
<script setup lang="ts" name="TabsOptionDialog">
import {useMessage, useMessageBox} from "/@/hooks/message";
import { getObj, addObj, putObj } from '/@/api/order/flow-application'
import { useI18n } from "vue-i18n"
import {onLoadDicUrl, onUpdateDicData} from "/@/flow/components/convert-name/convert";
import {DIC_PROP} from "../../support/dict-prop";
import {notifyLeft} from "../../index";
import {validateFormTypeSave} from "/@/api/order/order-key-vue";
const emit = defineEmits(['refresh']);
// 引入组件
const IconSelector = defineAsyncComponent(() => import('/@/components/IconSelector/index.vue'));
const { t } = useI18n();
// 定义变量内容
const dataFormRef = ref();
const visible = ref(false);
const loading = ref(false);
const operType = ref(false);
const title = ref('');
// 定义字典
const dicData = reactive({});
const onLoad = onLoadDicUrl({key: "groupName"});
const onUpdate = onUpdateDicData({key: "groupName"});
onMounted(() => {
onLoad(dicData);
});
// 提交表单数据
const form = reactive({
icon: '',
formName: '',
groupName: '',
path: '',
prop: '',
isActive: '1',
isAutoAudit: '0',
status: '-1',
version: 1,
sort: 1,
});
// 定义校验规则
const dataRules = ref({
icon: [{required: true, message: '图标不能为空', trigger: 'blur'}],
formName: [{required: true, message: '表单名称不能为空', trigger: 'blur'}],
groupName: [{required: true, message: '分组名称不能为空', trigger: 'blur'}],
prop: [{required: true, message: 'tab标识不能为空', trigger: 'blur'}],
isActive: [{required: true, message: '默认展示不能为空', trigger: 'blur'}],
isAutoAudit: [{required: true, message: '提交时自动审批不能为空', trigger: 'blur'}],
sort: [{required: true, message: '排序值不能为空', trigger: 'blur'}],
type: [{required: true, message: '表单类型不能为空', trigger: 'blur'}],
status: [{required: true, message: '状态不能为空', trigger: 'blur'}],
version: [{required: true, message: '版本不能为空', trigger: 'blur'}],
})
// 打开弹窗
const openDialog = (type: string, id: string) => {
visible.value = true
operType.value = type;
form.id = ''
if (type === 'add') {
title.value = t('common.addBtn');
} else if (type === 'edit') {
title.value = t('common.editBtn');
} else if (type === 'view') {
title.value = t('common.viewBtn');
}
// 重置表单数据
nextTick(() => {
dataFormRef.value?.resetFields();
});
// 获取TabsOption信息
if (id) {
form.id = id
getTabsOptionData(id)
}
};
function handleAddGroupName() {
useMessageBox().prompt("请输入新的分组名称")
.then(({ value }) => {
form.groupName = value
onUpdate(dicData, form);
})
}
// 提交
const onSubmit = async () => {
const valid = await dataFormRef.value.validate().catch(() => {});
if (!valid) return false;
if (form.type === DIC_PROP.FORM_TYPE[1].value) {
if (!form.path) {
notifyLeft('当前表单类型为系统表单【PC端路径】不能为空', 'warning')
return false
}
}
let isDesign = form.type !== DIC_PROP.FORM_TYPE[1].value;
if (isDesign && !form.formInfo) {
notifyLeft('当前表单类型为设计表单,请先完善第二项设计的表单内容', 'warning')
return false
}
validateFormTypeSave(form)
try {
loading.value = true;
form.id ? await putObj(form) : await addObj(form);
useMessage().success(t(form.id ? 'common.editSuccessText' : 'common.addSuccessText'));
visible.value = false;
emit('refresh');
} catch (err: any) {
useMessage().error(err.msg);
} finally {
loading.value = false;
}
};
// 初始化表单数据
const getTabsOptionData = (id: string) => {
// 获取数据
loading.value = true
getObj(id).then((res: any) => {
Object.assign(form, res.data)
}).finally(() => {
loading.value = false
})
};
// 暴露变量
defineExpose({
openDialog
});
</script>

View File

@@ -0,0 +1,140 @@
<template>
<div>
<div class="header">
<el-tabs v-model="form.active" @tab-click="activeMenu">
<el-tab-pane label="① 表单设置" name="tabsSetting"></el-tab-pane>
<el-tab-pane label="② 表单设计" name="formDesign"></el-tab-pane>
</el-tabs>
<div class="btn-publish" v-if="form.active !== 'formDesign'">
<el-button icon="CaretRight" type="primary" @click="activeMenu({paneName: 'formDesign'})">下一步
</el-button>
</div>
<div class="btn-publish" v-if="form.active === 'formDesign'">
<el-button icon="CirclePlus" type="primary" @click="publish('-1')">暂存
</el-button>
<el-button icon="Promotion" type="primary" @click="publish('1')">发布
</el-button>
</div>
<div class="btn-back">
<el-button @click="exitTabsForm" icon="Back" circle></el-button>
<span style="margin-left: 20px;">
<i :class="props.currFlowForm.icon"></i> <span>{{ props.currFlowForm.formName }}</span>
</span>
</div>
</div>
</div>
</template>
<script setup lang="ts" name="TabsDesignHeader">
import {useI18n} from "vue-i18n"
import {useMessageBox} from "/@/hooks/message";
const {t} = useI18n();
const $emit = defineEmits(["handleDesignTabs", "syncCurrFlowForm", "publish"]);
const props = defineProps({
currFlowForm: {
type: Object,
default: null,
}
});
const form = reactive({
active: "tabsSetting"
});
function publish(status) {
$emit('publish', form.active, () => {
props.currFlowForm[form.active] = true
}, status)
}
async function exitTabsForm() {
let text = ''
// 判断是否都保存
if (props.currFlowForm.formDesign !== true) text += '【表单设计】,'
if (props.currFlowForm.tabsSetting !== true) text += '【表单设置】'
if (!text) {
$emit("handleDesignTabs", false);
return
}
try {
await useMessageBox().confirm(text + '未保存,是否继续退出?');
} catch {
return;
}
$emit("handleDesignTabs", false);
}
function activeMenu(tab) {
let menu = tab.paneName
let active = form.active;
$emit("syncCurrFlowForm", (preSave)=>{
if (!preSave) menu = active
props.currFlowForm[menu] = false;
form.active = menu;
props.currFlowForm.active = menu;
});
}
// 关闭提示
function listenPage() {
window.onbeforeunload = function (e) {
e = e || window.event;
if (e) {
e.returnValue = "关闭提示";
}
return "关闭提示";
};
}
onMounted(() => {
listenPage();
});
</script>
<style lang="scss" scoped>
.header {
min-width: 980px;
.el-tabs {
position: absolute;
top: 15px;
z-index: 999;
display: flex;
justify-content: center;
width: 100%;
}
.btn-publish {
position: absolute;
top: 20px;
z-index: 1000;
right: 40px !important;
i {
margin-right: 6px;
}
button {
width: 74px;
height: 28px;
border-radius: 15px;
}
}
.btn-back {
position: absolute;
top: 20px;
z-index: 1000;
left: 20px !important;
font-size: 18px;
i {
margin-right: 6px;
}
button {
width: 44px;
height: 28px;
border-radius: 15px;
}
}
}
</style>
<style lang="scss" scoped>
</style>

View File

@@ -0,0 +1,42 @@
export default {
tabsOption: {
index: '#',
importtabsOptionTip: 'import TabsOption',
id: 'id',
icon: 'icon',
formName: 'formName',
groupName: 'groupName',
path: 'path',
sort: 'sort',
isActive: 'isActive',
isAutoAudit: 'isAutoAudit',
status: 'status',
version: 'version',
createTime: 'createTime',
createUser: 'createUser',
updateUser: 'updateUser',
formInfo: 'formInfo',
type: 'type',
updateTime: 'updateTime',
delFlag: 'delFlag',
inputIdTip: 'input id',
inputIconTip: 'input icon',
inputFormNameTip: 'input formName',
inputGroupNameTip: 'input groupName',
inputPathTip: 'input path',
inputSortTip: 'input sort',
inputIsActiveTip: 'input isActive',
inputIsAutoAuditTip: 'input isAutoAudit',
inputStatusTip: 'input status',
inputVersionTip: 'input version',
inputCreateTimeTip: 'input createTime',
inputCreateUserTip: 'input createUser',
inputUpdateUserTip: 'input updateUser',
inputFormInfoTip: 'input formInfo',
inputTypeTip: 'input type',
inputUpdateTimeTip: 'input updateTime',
inputDelFlagTip: 'input delFlag',
}
}

View File

@@ -0,0 +1,42 @@
export default {
tabsOption: {
index: '#',
importtabsOptionTip: '导入流程表单',
id: '主键ID',
icon: '图标',
formName: '表单名称',
groupName: '分组名称',
path: 'PC端路径',
sort: '排序值',
isActive: '默认展示',
isAutoAudit: '提交时自动审批',
status: '状态',
version: '版本',
createTime: '创建时间',
createUser: '创建人',
updateUser: '更新人',
formInfo: '表单信息',
type: '表单类型',
updateTime: '更新时间',
delFlag: '删除标识',
inputIdTip: '请输入主键ID',
inputIconTip: '请输入图标',
inputFormNameTip: '请输入表单名称',
inputGroupNameTip: '请输入分组名称',
inputPathTip: '请输入PC端路径',
inputSortTip: '请输入排序值',
inputIsActiveTip: '请输入默认展示',
inputIsAutoAuditTip: '请输入提交时自动审批',
inputStatusTip: '请输入状态',
inputVersionTip: '请输入版本',
inputCreateTimeTip: '请输入创建时间',
inputCreateUserTip: '请输入创建人',
inputUpdateUserTip: '请输入更新人',
inputFormInfoTip: '请输入表单信息',
inputTypeTip: '请选择表单类型',
inputUpdateTimeTip: '请输入更新时间',
inputDelFlagTip: '请输入删除标识',
}
}

View File

@@ -0,0 +1,226 @@
<template>
<div>
<el-form ref="dataFormRef" :model="props.currFlowForm" :rules="dataRules" label-width="150px"
v-loading="loading"
label-position="left"
:disabled="operType === 'view'">
<el-row :gutter="24">
<el-col :span="12" class="mb20" :offset="6">
<el-form-item :label="t('tabsOption.formName')" prop="formName">
<el-input v-model="props.currFlowForm.formName" :placeholder="t('tabsOption.inputFormNameTip')" maxlength="20" show-word-limit clearable/>
</el-form-item>
</el-col>
<el-col :span="12" class="mb20" :offset="6">
<el-form-item :label="t('tabsOption.icon')" prop="icon">
<IconSelector :placeholder="$t('tabsOption.inputIconTip')" v-model="props.currFlowForm.icon"/>
</el-form-item>
</el-col>
<el-col :span="12" class="mb20" :offset="6">
<el-form-item :label="t('tabsOption.groupName')" prop="groupName">
<el-select v-model="props.currFlowForm.groupName" style="width: 83%!important;"
:placeholder="t('tabsOption.inputGroupNameTip')" clearable
filterable allowCreate defaultFirstOption>
<el-option v-for="(item, index) in dicData.groupName" :key="index" :label="item.groupName"
:value="item.groupName"></el-option>
</el-select>
<el-button
type="primary" size="small" round style="margin-left: 10px"
@click="handleAddGroupName"
>新增分组
</el-button>
</el-form-item>
</el-col>
<el-col :span="12" class="mb20" :offset="6">
<el-form-item :label="t('tabsOption.type')" prop="type">
<el-select v-model="props.currFlowForm.type" :placeholder="t('tabsOption.inputTypeTip')" clearable filterable>
<el-option v-for="(item, index) in DIC_PROP.FORM_TYPE" :key="index" :label="item.label" :value="item.value"></el-option>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12" class="mb20" :offset="6" v-if="props.currFlowForm.type === DIC_PROP.FORM_TYPE[1].value">
<el-form-item :label="t('tabsOption.path')" prop="path">
<el-tooltip content="当表单类型为系统表单时可输入本地Vue组件路径或者外部系统http或https开头的页面路径" placement="top">
<el-input v-model="props.currFlowForm.path" :placeholder="t('tabsOption.inputPathTip')" clearable>
</el-input>
</el-tooltip>
</el-form-item>
</el-col>
<el-col :span="12" class="mb20" :offset="6">
<el-form-item :label="t('flowApplication.remark')" prop="remark">
<el-input v-model="props.currFlowForm.remark" type="textarea" :placeholder="t('flowApplication.inputRemarkTip')"/>
</el-form-item>
</el-col>
<el-col :span="12" class="mb20" :offset="6">
<el-form-item :label="t('tabsOption.sort')" prop="sort">
<el-input-number :min="1" :max="1000" v-model="props.currFlowForm.sort"
:placeholder="t('tabsOption.inputSortTip')"></el-input-number>
</el-form-item>
</el-col>
<el-col :span="12" class="mb20" :offset="6">
<el-form-item :label="t('tabsOption.version')" prop="version">
<el-input-number :min="1" :max="1000" v-model="props.currFlowForm.version" :placeholder="t('tabsOption.inputVersionTip')"></el-input-number>
</el-form-item>
</el-col>
<el-col :span="12" class="mb20" :offset="6">
<el-tooltip content="在审批时默认展示的第1个页面。当存在多个默认展示页面时优先展示排序值最小的" placement="top">
<el-form-item :label="t('tabsOption.isActive')" prop="isActive">
<el-radio-group v-model="props.currFlowForm.isActive">
<el-radio v-for="(item, index) in DIC_PROP.YES_OR_NO" :key="index" :label="item.value">
{{ getLabelByLanguage(item) }}
</el-radio>
</el-radio-group>
</el-form-item>
</el-tooltip>
</el-col>
<el-col :span="12" class="mb20" :offset="6">
<el-tooltip content="在审批时不会显示审批按钮,在页面点提交按钮会自动流转到下一步" placement="top">
<el-form-item :label="t('tabsOption.isAutoAudit')" prop="isAutoAudit">
<el-radio-group v-model="props.currFlowForm.isAutoAudit">
<el-radio v-for="(item, index) in DIC_PROP.YES_OR_NO" :key="index" :label="item.value" >
{{ getLabelByLanguage(item) }}
</el-radio>
</el-radio-group>
</el-form-item>
</el-tooltip>
</el-col>
<el-col :span="12" class="mb20" :offset="6">
<el-form-item :label="t('tabsOption.status')" prop="status">
<el-radio-group v-model="props.currFlowForm.status">
<el-radio v-for="(item, index) in DIC_PROP.TEMP_STATUS" :key="index" :label="item.value">
{{ getLabelByLanguage(item) }}
</el-radio>
</el-radio-group>
</el-form-item>
</el-col>
</el-row>
</el-form>
</div>
</template>
<script setup lang="ts" name="TabsOptionForm">
import {useMessage, useMessageBox} from "/@/hooks/message";
import {getObj, addObj, putObj} from '/@/api/order/flow-application'
import other from '/@/utils/other';
import {notifyLeft, stringifyWithFunctions} from "/@/flow";
import {useI18n} from "vue-i18n"
import {onLoadDicUrl, onUpdateDicData} from "/@/flow/components/convert-name/convert";
import {PROP_CONST} from "../../support/prop-const";
import {DIC_PROP} from "../../support/dict-prop";
import {validateFormTypeSave} from "/@/api/order/order-key-vue";
const {t} = useI18n();
const {proxy} = getCurrentInstance();
const props = defineProps({
currFlowForm: {
type: Object,
default: null,
}
});
// 引入组件
const IconSelector = defineAsyncComponent(() => import('/@/components/IconSelector/index.vue'));
// 定义变量内容
const dataFormRef = ref();
const loading = ref(false);
const operType = ref(false);
// 定义字典
const dicData = reactive({});
const onLoad = onLoadDicUrl({key: "groupName"});
const onUpdate = onUpdateDicData({key: "groupName"});
onMounted(async () => {
await onLoad(dicData);
let isNoGroupName = !props.currFlowForm.groupName && dicData.groupName.length > 0;
if (isNoGroupName) {
props.currFlowForm.groupName = dicData.groupName[0].groupName
}
});
// 定义校验规则
const dataRules = ref({
icon: [{required: true, message: '图标不能为空', trigger: 'blur'}],
type: [{required: true, message: '表单类型不能为空', trigger: 'blur'}],
formName: [{required: true, message: '表单名称不能为空', trigger: 'blur'}],
groupName: [{required: true, message: '分组名称不能为空', trigger: 'blur'}],
prop: [{required: true, message: 'tab标识不能为空', trigger: 'blur'}],
sort: [{required: true, message: '排序值不能为空', trigger: 'blur'}],
isActive: [{required: true, message: '默认展示不能为空', trigger: 'blur'}],
isAutoAudit: [{required: true, message: '提交时自动审批不能为空', trigger: 'blur'}],
status: [{required: true, message: '状态不能为空', trigger: 'blur'}],
version: [{required: true, message: '版本不能为空', trigger: 'blur'}],
})
function handleAddGroupName() {
useMessageBox().prompt("请输入新的分组名称")
.then(({ value }) => {
props.currFlowForm.groupName = value
onUpdate(dicData, props.currFlowForm);
})
}
async function validatePreSave() {
const valid = await dataFormRef.value.validate().catch(() => {});
if (!valid) {
notifyLeft('请先完善表单设置内容', 'warning')
return false;
}
let isSys = props.currFlowForm.type === DIC_PROP.FORM_TYPE[1].value;
if (isSys && !props.currFlowForm.path) {
notifyLeft('当前表单类型为系统表单【PC端路径】不能为空', 'warning')
return false
}
return true
}
function validateOtherSave() {
let isDesign = props.currFlowForm.type !== DIC_PROP.FORM_TYPE[1].value;
if (isDesign && !props.currFlowForm.formInfo) {
notifyLeft('当前表单类型为设计表单,请先完善第二项设计的表单内容', 'warning')
return false
}
return true
}
const handleSubmit = async (callback, status) => {
if (status) {
props.currFlowForm.status = status
}
let preSave = await validatePreSave();
if (!preSave) return
if (!validateOtherSave()) return
let formJson = other.deepClone(props.currFlowForm)
formJson.formInfo = stringifyWithFunctions(formJson.formInfo)
validateFormTypeSave(formJson)
try {
loading.value = true;
await onUpdate(dicData, props.currFlowForm);
await addObj(formJson)
props.currFlowForm.tabsSetting = true
notifyLeft('当前表单设置保存成功')
if (callback) callback()
} catch (err: any) {
useMessage().error(err);
} finally {
loading.value = false;
}
}
// 暴露变量
defineExpose({
handleSubmit, validatePreSave, validateOtherSave
});
</script>

View File

@@ -0,0 +1,378 @@
<template>
<div>
<el-form ref="dataFormRef" :model="props.currFlowForm" :rules="dataRules" label-width="80px"
v-loading="loading" v-if="!props.currFlowForm.isForm">
<el-row :gutter="24">
<el-col :span="12" class="mb20">
<el-form-item :label="t('formoption.formType')" prop="formType">
<el-select v-model="props.currFlowForm.formType" :placeholder="t('formoption.inputFormTypeTip')" clearable
filterable
@change="formTypeChange">
<el-option v-for="(item, index) in DIC_PROP.FORM_TYPE" :key="index" :label="item.label"
:value="item.value"></el-option>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12" class="mb20">
<el-form-item :label="t('formoption.formId')" prop="formId">
<el-select v-model="props.currFlowForm.formId" :placeholder="t('formoption.inputFormIdTip')" clearable
filterable
@change="formIdChange">
<el-option v-for="(item, index) in dicData.formIdByType" :key="index"
:label="item.formName" :value="item.id"></el-option>
</el-select>
</el-form-item>
</el-col>
</el-row>
</el-form>
<div style="margin-bottom: 10px; color: #72767b;">请拖拽字段到设计器中</div>
<el-container>
<el-aside width="200px">
<el-divider> 系统字段 </el-divider>
<template v-for="(item, index) in data.sysFields" :key="index">
<div :draggable="item.prop!=='_define_'" v-if="item.label"
class="tinymce-form-prop" @dragstart="dragNode(item)">
{{ item.label }}
</div>
</template>
<el-divider> 表单字段 </el-divider>
<template v-for="(item, index) in data.formFields" :key="index">
<div draggable="true" v-if="item.label"
class="tinymce-form-prop" @dragstart="dragNode(item)">
{{ item.label }}
</div>
</template>
</el-aside>
<el-main>
<editor v-model="props.currFlowForm.printInfo" @dragover="allowDrop" @drop="drop"
:init="init"
:disabled="loading"
:id="tinymceId">
</editor>
<footer class="el-dialog__footer">
<span class="dialog-footer">
<el-button @click="cancelClick">清空模板</el-button>
<el-button type="primary" @click="confirmClick">保存模板</el-button>
</span>
</footer>
</el-main>
</el-container>
</div>
</template>
<script setup lang="ts" name="TinymceEditor">
import tinymce from "tinymce/tinymce";
import Editor from "@tinymce/tinymce-vue";
import "tinymce/icons/default/icons";
import "tinymce/themes/silver";
import "tinymce/plugins/print";
import "tinymce/plugins/hr";
import "tinymce/plugins/anchor";
import "tinymce/plugins/fullscreen";
import "tinymce/plugins/pagebreak";
import "tinymce/plugins/image";
import "tinymce/plugins/table";
import "tinymce/plugins/lists";
import "tinymce/plugins/wordcount";
import "tinymce/plugins/preview";
import "tinymce/plugins/emoticons";
import "tinymce/plugins/emoticons/js/emojis.js";
import "tinymce/plugins/code";
import "tinymce/plugins/link";
import "tinymce/plugins/advlist";
import "tinymce/plugins/autoresize";
import "tinymce/plugins/quickbars";
import "tinymce/plugins/nonbreaking";
import "tinymce/plugins/searchreplace";
import "tinymce/plugins/autolink";
import "tinymce/plugins/directionality";
import "tinymce/plugins/visualblocks";
import "tinymce/plugins/visualchars";
import "tinymce/plugins/charmap";
import "tinymce/plugins/nonbreaking";
import "tinymce/plugins/insertdatetime";
import "tinymce/plugins/importcss";
import "tinymce/plugins/help";
import {buildFieldPerms} from "/@/flow/utils/form-perm";
import {validateNull} from "/@/utils/validate";
import {useMessage} from "/@/hooks/message";
import {PROP_CONST} from "../../support/prop-const";
import {onLoadDicUrl} from "../convert-name/convert";
import {DIC_PROP} from "../../support/dict-prop";
import {listPrintTemp, savePrintTemp} from "/@/api/jsonflow/form-option";
import {useI18n} from "vue-i18n";
import {parseWithFunctions} from "../../index";
import {deepClone} from "/@/utils/other";
const $message = useMessage();
const { t } = useI18n()
const props = defineProps({
currFlowForm: {
type: Object,
default: null,
},
baseUrl: {
type: String,
default: "",
},
plugins: {
type: [String, Array],
default:
"print preview hr anchor fullscreen pagebreak help searchreplace autoresize quickbars autolink directionality code visualblocks visualchars image link table nonbreaking charmap insertdatetime advlist lists wordcount emoticons help",
},
toolbar: {
type: [String, Array],
default:
"code undo redo | cut copy paste pastetext | forecolor backcolor bold italic underline strikethrough link anchor | alignleft aligncenter alignright alignjustify outdent indent | \
blocks fontfamily fontsize | bullist numlist | blockquote subscript superscript removeformat | \
table image charmap pagebreak insertdatetime emoticons hr | print preview fullscreen | styleselect formatselect fontselect fontsizeselect",
},
});
const loading = ref(false);
const tinymceId = ref(
"tinymce-id-" + +new Date() + ((Math.random() * 1000).toFixed(0) + "")
);
// 定义init初始化对象
const init = reactive({
selector: "#" + tinymceId.value,
language_url: "/flow/tinymce/langs/zh_CN.js",
language: "zh_CN",
skin_url: "/flow/tinymce/skins/ui/oxide",
branding: false,
promotion: false,
menubar: "file edit view insert format tools table help",
paste_data_images: true,
image_dimensions: false,
plugins: props.plugins,
toolbar: props.toolbar,
quickbars_image_toolbar: "alignleft aligncenter alignright | rotateleft rotateright | imageoptions",
editimage_toolbar: "rotateleft rotateright | flipv fliph | editimage imageoptions",
font_formats: "Arial=arial,helvetica,sans-serif; 宋体=SimSun; 微软雅黑=Microsoft Yahei; Impact=impact,chicago;",
fontsize_formats: "11px 12px 14px 16px 18px 24px 36px 48px 64px 72px",
image_caption: true,
editimage_cors_hosts: ["picsum.photos"],
noneditable_class: "mceNonEditable",
toolbar_mode: "wrap",
content_style: "body { font-family:Helvetica,Arial,sans-serif; font-size:16px }",
image_advtab: true,
importcss_append: true,
paste_webkit_styles: "all",
paste_merge_formats: true,
nonbreaking_force_tab: false,
paste_auto_cleanup_on_paste: false,
file_picker_types: "file",
quickbars_insert_toolbar: "",
quickbars_selection_toolbar: "bold italic | quicklink h2 h3 blockquote quickimage quicktable",
autoresize_bottom_margin: 50,
autoresize_max_height: 500,
autoresize_min_height: 350,
content_css: "/flow/tinymce/skins/content/default/content.css",
images_upload_handler: (blobInfo, success, failure) => {
// base64形式上传图片
const img = 'data:image/jpeg;base64,' + blobInfo.base64()
success(img)
}
});
const data = reactive({
formFields: [],
sysFields: deepClone(PROP_CONST.SYS_FIELDS),
})
const prop = reactive({
propId: null,
prop: null,
propVar: null,
propType: null,
label: null
});
// 开始拖拽
function dragNode(item) {
prop.propId = item.propId
prop.prop = item.prop
prop.propVar = "${" + item.prop + "}"
prop.subForm = item.subForm
prop.propType = item.propType
prop.label = item.label
}
function allowDrop(e) {
tinymce.activeEditor.selection.select(e.target, true).focus()
tinymce.activeEditor.selection.collapse(false)
e.preventDefault();
}
function drop(e) {
let tagName = e.target.tagName;
// 判断子表单信息
if (prop.subForm && tagName !== 'TD') {
$message.warning('子表单字段只能拖拽到表格中,请先创建表格');
return
} else {
// 处理子表单
if (prop.propType && prop.propType.indexOf(PROP_CONST.FORM_DESIGN.subForm) !== -1) {
// TODO 创建对应列的表格
}
}
let content = prop.propVar
tinymce.activeEditor.insertContent(content)
}
function extractedFormFields(formFields) {
formFields.forEach(each => {
if (each.subForm) {
each.prop = each.subForm + '.' + each.prop
each.label += "(子表单)"
}
})
data.formFields = formFields
}
function buildFormFieldPerms(formInfoStr) {
if (validateNull(formInfoStr)) return
let formInfo = parseWithFunctions(formInfoStr, true)
let formFields = []
buildFieldPerms(formFields, formInfo.widgetList);
extractedFormFields(formFields)
}
// 定义查询字典
const dicData = reactive({});
const onLoad = onLoadDicUrl({key: "formId"});
// 定义校验规则
const dataRules = ref({
formType: [{required: true, message: '表单来源不能为空', trigger: 'blur'}],
formId: [{required: true, message: '表单名称不能为空', trigger: 'blur'}],
})
const formTypeChange = (value, isInit) => {
if (value) {
dicData.formIdByType = dicData.formId.filter(f => f.type === value)
} else dicData.formIdByType = []
clearFormFields()
if (!isInit) props.currFlowForm.formId = null
}
const formIdChange = (value) => {
clearFormFields()
if (!value) return
let find = dicData.formIdByType.find(f => f.id === value);
// 表单实例
if (!validateNull(props.currFlowForm.formInfo)) {
handleFormPrintDef(value, props.currFlowForm.type, props.currFlowForm)
return;
}
props.currFlowForm.formName = find.formName
props.currFlowForm.path = find.path
handleFormPrintDef(value, props.currFlowForm.type, find)
}
function clearFormFields(){
data.formFields = [];
props.currFlowForm.printInfo = null
tinymce.activeEditor.setContent('')
}
// 查询字段信息
function handleFormPrintDef(formId, type, find) {
clearFormFields()
listPrintTemp({
flowInstId: props.currFlowForm.flowInstId,
type: type, formType: props.currFlowForm.formType, formId: formId,
}).then(resp => {
let res = resp.data;
props.currFlowForm.printInfo = res.printInfo
if (!validateNull(res.columns)) extractedFormFields(res.columns)
else if (!validateNull(find.formInfo)) {
buildFormFieldPerms(find.formInfo);
} else if (props.currFlowForm.formType === DIC_PROP.FORM_TYPE[0].value){
$message.warning("当前选择的设计表单无字段信息,请先在《表单设计器》中设计")
} else {
$message.warning("当前选择的系统表单无字段信息,请先在表单设计中录入")
}
})
.catch(() => {
$message.error("获取表单字段定义失败");
})
}
async function initFormParams() {
await onLoad(dicData);
// 初始化
props.currFlowForm.type = DIC_PROP.FORM_DATA_TYPE[2].value
let formId = props.currFlowForm.formId;
if (formId) {
formTypeChange(props.currFlowForm.formType, true)
formIdChange(formId)
}
}
async function confirmClick() {
await savePrintTemp(props.currFlowForm)
useMessage().success(t('common.editSuccessText'));
}
async function cancelClick() {
props.currFlowForm.printInfo = null
tinymce.activeEditor.setContent('')
await confirmClick()
}
// 监听变化
watch(
() => props.currFlowForm.id,
async () => {
await initFormParams()
}
);
// 初始化
onMounted(async () => {
await initFormParams()
await tinymce.init({});
});
</script>
<style lang="scss" scoped>
.el-dialog__footer {
text-align: center;
margin-top: 10px;
.dialog-footer {
text-align: center;
}
}
</style>
<style lang="scss">
@import "/flow/tinymce/skins/content/default/content.css";
.tox-tinymce {
border: 1px solid #dcdfe6;
border-radius: 4px;
.tox-statusbar {
display: none;
}
}
/* 在el-dialog中z-index被遮挡 */
.tox-tinymce-aux {
z-index: 9999 !important;
}
.tinymce-form-prop {
cursor: move;
background: rgb(245, 246, 246);
border: 1px solid rgb(245, 246, 246);
border-radius: 8px;
margin-bottom: 5px;
padding: 8px;
&:hover {
color: #0960bd;
outline: 1px dashed #0960bd;
border: 1px solid #0960bd;
}
}
</style>

View File

@@ -0,0 +1,246 @@
<template>
<div>
<div id="printInfo" style="word-break: break-all;" v-html="printInfoHtml"></div>
<div>
<footer class="el-dialog__footer" style="text-align: center;">
<span class="dialog-footer">
<el-button type="primary" @click="confirmClick">打印</el-button>
</span>
</footer>
</div>
</div>
</template>
<script setup lang="ts" name="TinymceView">
import {printHtml, validateRunFlowId} from "/@/flow";
import {validateNull} from "/@/utils/validate";
import {onFormLoadedUrl, onLoadDicUrl} from "../convert-name/convert";
import {PROP_CONST} from "../../support/prop-const";
import {handleFormatValue, handlePrintValue} from "../form-create";
import {buildFieldPerms} from "../../utils/form-perm";
import {listFormOption} from "/@/api/jsonflow/form-option";
import {useMessage} from "/@/hooks/message";
import {DIC_PROP} from "../../support/dict-prop";
const $message = useMessage();
const props = defineProps({
currFlowForm: {
type: Object,
default: null,
},
elTab: {
type: Object,
default: null,
}
});
// 定义字典
const dicData = reactive({});
const onLoad = onLoadDicUrl();
const onFormLoaded = onFormLoadedUrl();
const printInfoHtml = ref(null)
const data = reactive({
formFields: [],
// 自定义字段
formDefFields: [],
});
async function initPrintInfoHtml(printInfo) {
if (props.currFlowForm.rule) {
buildFieldPerms(data.formFields, props.currFlowForm.rule, null, true)
} else {
// 初始化自定义字段
data.formDefFields = handleCustomFormDef()
}
buildPrintInfoHtml(printInfo)
}
// 查询字段信息
async function handleCustomFormDef() {
if (validateNull(props.elTab)) return []
// 判断流程实例独立配置
let form = validateRunFlowId(props, {})
let resp = await listFormOption({
flowInstId: form.flowInstId,
type: DIC_PROP.FORM_DATA_TYPE[0].value, formType: data.elTab.type, formId: data.elTab.id
}).catch(() => {
$message.error("获取表单字段定义失败");
})
return resp.data;
}
async function buildPrintInfoHtml(printInfo) {
let regTable = /<\/tbody>.*?/g;
let regTr = /<tr>.*?<\/tr>/g;
let regSubVar = /\$\{.*?\..*?\}/g;
let regVar = /\$\{.*?\}/g;
printInfo = printInfo.replaceAll('\n', '')
let tables = printInfo.split(regTable)
for (let j = 0; j < tables.length; j++) {
let table = tables[j]
regSubVar.lastIndex = 0
if (regSubVar.test(table)) {
let newTable = table.replace(regTr, function (str) {
// 确保完整匹配
regSubVar.lastIndex = 0
if (regSubVar.test(str)) {
let propVar = str.substr(str.indexOf("${"), str.indexOf("}") - str.indexOf("${") + 1)
let prop = propVar.substring(propVar.indexOf("${") + 2, propVar.indexOf("."))
// 获取表格数据
let tableData = props.currFlowForm.formData[prop]
// 构造动态数据行
let dynamicRows = "";
if (!validateNull(tableData)) {
for (let i = 0; i < tableData.length; i++) {
let dynStr = str.replace(regSubVar, function (str) {
let propVar = str.substr(str.indexOf("${"), str.indexOf("}") - str.indexOf("${") + 1)
let prop = propVar.substring(propVar.indexOf(".") + 1, propVar.indexOf("}"))
let existSubForm = validateSubFormField(props.currFlowForm.modelRefList, prop)
let parProp = propVar.substring(propVar.indexOf("${") + 2, propVar.indexOf("}"))
let formDatum = tableData[i][prop]
if (existSubForm) {
// 判断自定义组件
let printValue = existSubForm.__fc__.prop.props.formCreateInject.printValue
if (!printValue) {
let optionItems = existSubForm.__fc__.prop.options
printValue = handlePrintValue(optionItems, formDatum, 'value', 'label', parProp, existSubForm.type)
}
if (printValue) return printValue
}
if (!existSubForm) existSubForm = data.formFields.find(f => f.prop === parProp);
// 自定义字段
if (!existSubForm) existSubForm = data.formDefFields.find(f => f.subForm + '.' + f.prop === parProp);
let propType = existSubForm ? existSubForm.propType || existSubForm.type : null
return formDatum ? handleFormatValue(formDatum, formDatum, parProp, propType) : ''
})
dynamicRows += dynStr;
}
}
return dynamicRows;
} else {
return str;
}
})
printInfo = printInfo.replace(table, newTable);
}
}
// 替换外层变量值
if (regVar.test(printInfo)) {
// 确保完整匹配
regVar.lastIndex = 0
const matches = [];
let match;
while ((match = regVar.exec(printInfo)) !== null) {
matches.push({index: match.index, fullMatch: match[0], length: match[0].length});
}
// 为每个匹配项创建异步任务
const promises = matches.map(async (match) => {
const propVar = match.fullMatch;
let prop = propVar.substring(propVar.indexOf("${") + 2, propVar.indexOf("}"))
let formDatum = props.currFlowForm.formData[prop];
let isSysFields = false, existForm = null;
if (props.currFlowForm.rule) {
existForm = data.formFields.find(f => f.prop === prop);
if (existForm) {
// 判断自定义组件
let printValue = existForm.__fc__.prop.props.formCreateInject.printValue
if (!printValue) {
let optionItems = existForm.__fc__.prop.options
printValue = handlePrintValue(optionItems, formDatum, 'value', 'label', prop, existForm.propType)
}
if (printValue) return printValue;
} else {
isSysFields = true;
}
} else {
let optionItems = props.currFlowForm.dicData[prop];
if (!validateNull(optionItems)) {
let valueKey = props.currFlowForm[prop + '.valueKey']
let showKey = props.currFlowForm[prop + '.showKey']
existForm = data.formDefFields.find(f => f.prop === prop);
let propType = existForm ? existForm.propType : null
let printValue = handlePrintValue(optionItems, formDatum, valueKey, showKey, prop, propType)
if (printValue) return printValue;
} else {
isSysFields = true;
}
}
if (isSysFields) {
formDatum = props.currFlowForm[prop];
let field = PROP_CONST.SYS_FIELDS.find(f => f.prop === prop);
if (!validateNull(field) && field.valueKey) {
await onFormLoaded(dicData, {[prop]: formDatum}, {key: prop});
let find = dicData[prop].find(f => f[field.valueKey] === formDatum);
existForm = data.formDefFields.find(f => f.prop === prop);
let propType = existForm ? existForm.propType : null
if (find) return handleFormatValue(find[field.showKey], find[field.showKey], prop, propType)
}
}
let propType = existForm ? existForm.propType : null
return formDatum ? handleFormatValue(formDatum, formDatum, prop, propType) : ''
})
const replacements = await Promise.all(promises);
// 从后往前替换,避免索引变化问题
let dynStr = printInfo;
for (let i = matches.length - 1; i >= 0; i--) {
const match = matches[i];
const replacement = replacements[i];
dynStr = dynStr.substring(0, match.index) + replacement + dynStr.substring(match.index + match.length);
}
printInfo = dynStr;
}
printInfoHtml.value = printInfo
}
function validateSubFormField(modelRefList, field) {
if (validateNull(modelRefList)) return null
let resExistField = null
for (const each of modelRefList) {
if (validateNull(each.model())) continue
let existField = each.model()[field];
if (existField) {
resExistField = existField
break;
}
}
return resExistField
}
function confirmClick() {
printHtml("printInfo", props.currFlowForm.formName, null)
}
// 监听变化
watch(
() => props.currFlowForm.id,
() => {
initPrintInfoHtml(props.currFlowForm.printInfo);
}
);
// 初始化
onMounted(async () => {
// await onLoad(dicData);
initPrintInfoHtml(props.currFlowForm.printInfo);
});
</script>
<style lang="scss">
// 排除tailwind.css全局样式的影响
/*border-width: 0;
border-style: solid;*/
#printInfo {
table, th, td {
border: 1px solid; /* 设置边框样式 */
border-collapse: collapse; /* 合并边框 */
}
}
/*img { TODO 本地设置默认大小
width: 660px;
height: 150px;
}*/
</style>

View File

@@ -0,0 +1,89 @@
<template>
<div style="width: 100%" v-if="data.updateMultiple">
<el-tooltip content="请输入部门名称进行模糊搜索" placement="top">
<el-select v-model="data.modelValue" clearable filterable @change="changeModelValue"
remote :remote-method="remoteMethod" :reserve-keyword="false"
:multiple="props.multiple" :disabled="props.disabled">
<el-option v-for="(item, index) in dicData.depts" :key="index" :label="item.name"
:value="item.deptId"></el-option>
</el-select>
</el-tooltip>
</div>
</template>
<script setup lang="ts" name="UserRolePickerIndex">
// 定义字典
import {onFormLoadedUrl, onLoadDicUrl, remoteMethodByKey} from "/@/flow/components/convert-name/convert";
import {validateNull} from "/@/utils/validate";
import {handlePrintValue} from "/@/flow/components/form-create";
const props = defineProps({
formCreateInject: {
type: Object,
default: {},
},
modelValue: {
type: [Number, String, Array],
default: null,
},
multiple: {
type: Boolean,
default: false,
},
disabled: {
type: Boolean,
default: false,
},
});
const emits = defineEmits(['update:modelValue']);
const dicData = reactive({});
const onLoad = onLoadDicUrl();
const onFormLoaded = onFormLoadedUrl({key: "depts", field: "modelValue"});
onMounted(async () => {
data.modelValue = props.modelValue
await onFormLoaded(dicData, data);
changePrintValue()
});
const data = reactive({
modelValue: null,
updateMultiple: true
})
function remoteMethod(query: string) {
remoteMethodByKey(query, onLoad, dicData, 'deptName', "depts")
}
// 注入打印设计的内容,该逻辑非必须
const changePrintValue = () => {
if (!validateNull(data.modelValue)) {
let printValue = handlePrintValue(dicData.depts, data.modelValue, 'deptId', 'name');
if (printValue) props.formCreateInject.printValue = printValue
else props.formCreateInject.printValue = ""
} else {
props.formCreateInject.printValue = ""
}
}
const changeModelValue = (value) => {
data.modelValue = value
emits('update:modelValue', value);
changePrintValue()
}
// 监听双向绑定
watch(
() => props.multiple,
(val) => {
data.updateMultiple = false
changeModelValue(null)
setTimeout(()=>{
// 防止切换时报错
data.updateMultiple = true
})
}
);
</script>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,89 @@
<template>
<div style="width: 100%" v-if="data.updateMultiple">
<el-tooltip content="请输入岗位名称进行模糊搜索" placement="top">
<el-select v-model="data.modelValue" clearable filterable @change="changeModelValue"
remote :remote-method="remoteMethod" :reserve-keyword="false"
:multiple="props.multiple" :disabled="props.disabled">
<el-option v-for="(item, index) in dicData.posts" :key="index" :label="item.postName"
:value="item.postId"></el-option>
</el-select>
</el-tooltip>
</div>
</template>
<script setup lang="ts" name="UserRolePickerIndex">
// 定义字典
import {onFormLoadedUrl, onLoadDicUrl, remoteMethodByKey} from "/@/flow/components/convert-name/convert";
import {validateNull} from "/@/utils/validate";
import {handlePrintValue} from "/@/flow/components/form-create";
const props = defineProps({
formCreateInject: {
type: Object,
default: {},
},
modelValue: {
type: [Number, String, Array],
default: null,
},
multiple: {
type: Boolean,
default: false,
},
disabled: {
type: Boolean,
default: false,
},
});
const emits = defineEmits(['update:modelValue']);
const dicData = reactive({});
const onLoad = onLoadDicUrl();
const onFormLoaded = onFormLoadedUrl({key: "posts", field: "modelValue"});
onMounted(async () => {
data.modelValue = props.modelValue
await onFormLoaded(dicData, data);
changePrintValue()
});
const data = reactive({
modelValue: null,
updateMultiple: true
})
function remoteMethod(query: string) {
remoteMethodByKey(query, onLoad, dicData, 'postName', "posts")
}
// 注入打印设计的内容,该逻辑非必须
const changePrintValue = () => {
if (!validateNull(data.modelValue)) {
let printValue = handlePrintValue(dicData.posts, data.modelValue, 'postId', 'postName');
if (printValue) props.formCreateInject.printValue = printValue
else props.formCreateInject.printValue = ""
} else {
props.formCreateInject.printValue = ""
}
}
const changeModelValue = (value) => {
data.modelValue = value
emits('update:modelValue', value);
changePrintValue()
}
// 监听双向绑定
watch(
() => props.multiple,
(val) => {
data.updateMultiple = false
changeModelValue(null)
setTimeout(()=>{
// 防止切换时报错
data.updateMultiple = true
})
}
);
</script>

View File

@@ -0,0 +1,89 @@
<template>
<div style="width: 100%" v-if="data.updateMultiple">
<el-tooltip content="请输入角色名称进行模糊搜索" placement="top">
<el-select v-model="data.modelValue" clearable filterable @change="changeModelValue"
remote :remote-method="remoteMethod" :reserve-keyword="false"
:multiple="props.multiple" :disabled="props.disabled">
<el-option v-for="(item, index) in dicData.roles" :key="index" :label="item.roleName"
:value="item.roleId"></el-option>
</el-select>
</el-tooltip>
</div>
</template>
<script setup lang="ts" name="UserRolePickerIndex">
// 定义字典
import {onFormLoadedUrl, onLoadDicUrl, remoteMethodByKey} from "/@/flow/components/convert-name/convert";
import {validateNull} from "/@/utils/validate";
import {handlePrintValue} from "/@/flow/components/form-create";
const props = defineProps({
formCreateInject: {
type: Object,
default: {},
},
modelValue: {
type: [Number, String, Array],
default: null,
},
multiple: {
type: Boolean,
default: false,
},
disabled: {
type: Boolean,
default: false,
},
});
const emits = defineEmits(['update:modelValue']);
const dicData = reactive({});
const onLoad = onLoadDicUrl();
const onFormLoaded = onFormLoadedUrl({key: "roles", field: "modelValue"});
onMounted(async () => {
data.modelValue = props.modelValue
await onFormLoaded(dicData, data);
changePrintValue()
});
const data = reactive({
modelValue: null,
updateMultiple: true
})
function remoteMethod(query: string) {
remoteMethodByKey(query, onLoad, dicData, 'roleName', "roles")
}
// 注入打印设计的内容,该逻辑非必须
const changePrintValue = () => {
if (!validateNull(data.modelValue)) {
let printValue = handlePrintValue(dicData.roles, data.modelValue, 'roleId', 'roleName');
if (printValue) props.formCreateInject.printValue = printValue
else props.formCreateInject.printValue = ""
} else {
props.formCreateInject.printValue = ""
}
}
const changeModelValue = (value) => {
data.modelValue = value
emits('update:modelValue', value);
changePrintValue()
}
// 监听双向绑定
watch(
() => props.multiple,
(val) => {
data.updateMultiple = false
changeModelValue(null)
setTimeout(()=>{
// 防止切换时报错
data.updateMultiple = true
})
}
);
</script>

View File

@@ -0,0 +1,89 @@
<template>
<div style="width: 100%" v-if="data.updateMultiple">
<el-tooltip content="请输入用户名称进行模糊搜索" placement="top">
<el-select v-model="data.modelValue" clearable filterable @change="changeModelValue"
remote :remote-method="remoteMethod" :reserve-keyword="false"
:multiple="props.multiple" :disabled="props.disabled">
<el-option v-for="(item, index) in dicData.users" :key="index" :label="item.name"
:value="item.userId"></el-option>
</el-select>
</el-tooltip>
</div>
</template>
<script setup lang="ts" name="UserRolePickerIndex">
// 定义字典
import {onFormLoadedUrl, onLoadDicUrl, remoteMethodByKey} from "/@/flow/components/convert-name/convert";
import {validateNull} from "/@/utils/validate";
import {handlePrintValue} from "/@/flow/components/form-create";
const props = defineProps({
formCreateInject: {
type: Object,
default: {},
},
modelValue: {
type: [Number, String, Array],
default: null,
},
multiple: {
type: Boolean,
default: false,
},
disabled: {
type: Boolean,
default: false,
},
});
const emits = defineEmits(['update:modelValue']);
const dicData = reactive({});
const onLoad = onLoadDicUrl();
const onFormLoaded = onFormLoadedUrl({key: "users", field: "modelValue"});
onMounted(async () => {
data.modelValue = props.modelValue
await onFormLoaded(dicData, data);
changePrintValue()
});
const data = reactive({
modelValue: null,
updateMultiple: true
})
function remoteMethod(query: string) {
remoteMethodByKey(query, onLoad, dicData, 'userName', "users")
}
// 注入打印设计的内容,该逻辑非必须
const changePrintValue = () => {
if (!validateNull(data.modelValue)) {
let printValue = handlePrintValue(dicData.users, data.modelValue, 'userId', 'name');
if (printValue) props.formCreateInject.printValue = printValue
else props.formCreateInject.printValue = ""
} else {
props.formCreateInject.printValue = ""
}
}
const changeModelValue = (value) => {
data.modelValue = value
emits('update:modelValue', value);
changePrintValue()
}
// 监听双向绑定
watch(
() => props.multiple,
(val) => {
data.updateMultiple = false
changeModelValue(null)
setTimeout(()=>{
// 防止切换时报错
data.updateMultiple = true
})
}
);
</script>

View File

@@ -0,0 +1,100 @@
import request from "/@/utils/request"
export function predictFlow(obj?: Object, query?: Object) {
return request({
url: '/jsonflow/run-flow/predict',
method: 'post',
data: obj,
params: query
})
}
export function delFlowInfo(obj: any) {
return request({
url: '/jsonflow/run-flow/del/flow/info',
method: 'delete',
data: obj
})
}
export function getNodesByIdType(id, flowType, isEdit) {
// flowType: 0 模板 1 实例
// viewType: 0 查看 1 编辑
if (flowType === '1') return getNodesByFlowInstId(id, isEdit)
else return getNodesByDefFlowId(id)
}
export function getNodesByDefFlowId(id) {
return request({
url: '/jsonflow/def-flow/nodes/' + id,
method: 'get'
})
}
export function getNodesByFlowInstId(id, isEdit) {
return request({
url: '/jsonflow/run-flow/nodes/' + id + "/" + isEdit,
method: 'get'
})
}
export function addObjByDefFlowId(obj) {
return request({
url: '/jsonflow/def-flow',
method: 'post',
data: obj
})
}
export function addObjByFlowInstId(obj) {
return request({
url: '/jsonflow/run-flow/flow-inst-id',
method: 'post',
data: obj
})
}
export function listRunFlowsByDefFlowId(defFlowId, version) {
return request({
url: '/jsonflow/run-flow/def-flow-id/' + defFlowId + "/" + version,
method: 'get'
})
}
export function listDefFlowByFlowKey(flowKey) {
return request({
url: '/jsonflow/def-flow/flow-key/' + flowKey,
method: 'get'
})
}
export function listFlowApplicationByFlowKey(flowKey) {
return request({
url: '/order/flow-application/flow-key/' + flowKey,
method: 'get'
})
}
export function fetchComment(query) {
return request({
url: '/jsonflow/comment/comment/list',
method: 'get',
params: query
})
}
export function fetchRunJobs(flowInstId) {
return request({
url: '/jsonflow/run-job/list/' + flowInstId,
method: 'get'
})
}
export function fetchRunRejects(flowInstId) {
return request({
url: '/jsonflow/run-reject/list/' + flowInstId,
method: 'get'
})
}

View File

@@ -0,0 +1,76 @@
.child-ul-wrapper {
padding-left: 1px !important;
}
.jf-wrap-paper {
width: 100%;
height: 100%;
//position: relative;
position: absolute;
overflow: auto;
}
#jfDragWrapPaper {
width: 100%;
height: 100%;
position: relative;
overflow: hidden;
}
.jsonflow-navigator{
position: absolute;
bottom: 12px;
right: 8px;
border: #ccc solid 1px;
overflow: hidden;
.minimap-view{
position: absolute;
left: 2px;
top: 2px;
border: 2px dotted #999;
cursor: move;
}
}
.jf-wrap-paper-active {
background-color: #e4e4e4;
cursor: crosshair;
}
.container-scale {
position: absolute;
top: 10px;
right: 10px;
> span {
display: inline-block;
width: 40px;
text-align: center;
font-size: 14px;
color: rgba(0, 0, 0, .65);
}
/*缩放按钮*/
.el-button--small.is-circle {
padding: 5px 5px;
}
}
.horizontal-line-x {
position: absolute;
//width: 5000px;
height: 1px;
background: #409EFF;
//margin-left: -2500px;
display: none;
}
.vertical-line-y {
position: absolute;
width: 1px;
//height: 5000px;
background: #409EFF;
//margin-top: -2500px;
display: none;
}

View File

@@ -0,0 +1,63 @@
.flow-attr {
/*属性面板*/
.el-form-item {
margin: 0 15px 10px!important;
}
/*属性面板在一行*/
.el-form--label-top .el-form-item__label{
display: inline-block;
text-align: left;
padding: 0 0 10px!important;
}
/*tab选中样式*/
.el-tabs__active-bar {
background-color: revert;
}
/*tab头居中*/
.el-tabs__header{
margin: 8px 20px!important;
}
.el-form-item__content {
.el-select {
width: 303px!important;
}
}
.input-attr {
width: 303px;
.el-input-group__prepend {
background-color: #F5F7FA;
width: 97px;
}
}
.flow-config-attr {
margin-left: 5px;
}
.flow-param-attr {
margin-left: 25px;
}
.audit-endpoint {
position: absolute;
bottom: 0;
left: 200px;
}
.audit-endpoint-extract {
position: absolute;
bottom: 9px;
left: 150px;
}
}

View File

@@ -0,0 +1,93 @@
.flow-container {
height: 100%;
-moz-user-select: none;
-webkit-user-select: none;
-ms-user-select: none;
user-select: none;
}
.select-area {
position: relative;
z-index: 1001;
box-shadow: 0 3px 5px #ddd;
width: 54px;
background: #fff;
padding-top: 6px;
.el-row-tab {
text-align: center;
margin-bottom: 10px;
font-size: 12px;
height: 26px;
line-height: 24px;
margin-top: 7px;
margin-left: 17px;
}
/*左侧菜单栏*/
.el-row {
padding-bottom: 10px;
}
}
.header-option {
background: #fff;
height: 46px;
display: flex;
justify-content: space-between;
align-items: center;
box-shadow: 0 3px 5px #ddd;
position: relative;
z-index: 1000;
&__tools {
align-items: center;
display: flex;
width: 570px;
.el-select__wrapper {
border-radius: 16px;
}
}
.el-button {
height: 30px;
border-radius: 16px;
font-size: 12px;
}
.el-button+.el-button {
margin-left: 0;
}
.el-input__wrapper {
height: 30px;
border-radius: 16px;
}
.el-input__inner {
width: 70px;
}
}
.header-view-option {
background: #fff;
height: 46px;
display: flex;
justify-content: space-between;
align-items: center;
position: relative;
z-index: 1000;
&__tools {
}
}
.header-option-button {
border: 0;
padding: 8px 8px;
}
.flow-content {
background: #fafafa;
height: 100%;
border: 1px dashed rgba(170, 170, 170, 0.7);
padding: 0;
}

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,15 @@
import {nodeJobSvgIcons as nodeJobPath} from "./path";
// SVG ICON图标
export const nodeJobSvgIcons = {
serial: nodeJobPath.serial,
serialGate: nodeJobPath.serialGate
,parallel: nodeJobPath.parallel
,parallelGate: nodeJobPath.parallelGate
,job: nodeJobPath.job
,virtual: '/flow/menu/virtual-icon.svg'
,startMenu: '/flow/menu/start.svg'
,endMenu: '/flow/menu/end.svg'
,xLaneMenu: '/flow/menu/x-lane.svg'
,yLaneMenu: '/flow/menu/y-lane.svg'
}

View File

@@ -0,0 +1,17 @@
// SVG ICON图标
export const nodeJobSvgIcons = {
start: "/flow/icons/start-icon.svg"
,arrow: "/flow/icons/arrow-icon.svg"
,serial: "/flow/icons/serial-icon.svg"
,serialGate: "/flow/icons/serialGate.svg"
,parallel: "/flow/icons/parallel-icon.svg"
,parallelGate: "/flow/icons/parallelGate.svg"
,linkIcon: "/flow/icons/link-icon.svg"
,job: "/flow/icons/job-icon.svg"
,virtual: "/flow/icons/virtual-icon.svg"
,success: "/flow/icons/success.svg"
,warning: "/flow/icons/warning.svg"
,loading: "/flow/icons/loading.svg"
}
window._shapesSvgPath = nodeJobSvgIcons

View File

@@ -0,0 +1,138 @@
<template>
<div id="jfDragWrapPaper">
<div
id="jfWrapPaper"
class="jf-wrap-paper"
>
<div id="jsonflowMainPaper"></div>
</div>
<div id="minimapPaperView" class="jsonflow-navigator"></div>
<div class="container-scale">
<el-button icon="ZoomIn" circle size="small" type="default" @click="methods.enlargePaper"></el-button>
<span>{{ data.container.scaleShow }}% </span>
<el-button icon="ZoomOut" circle size="small" type="default" @click="methods.narrowPaper"></el-button>
</div>
<vue-context-menu
:contextMenuData="data.nodeContMenuData"
>
</vue-context-menu>
<vue-context-menu
:contextMenuData="data.linkContMenuData"
>
</vue-context-menu>
</div>
</template>
<script setup lang="ts" name="FlowAreaView">
import {flowConfig} from "../config/flow-config";
import {utils} from "../utils/common";
import {deepClone} from "/@/utils/other";
import {useI18n} from "vue-i18n";
import {validateNull} from "/@/utils/validate";
import {hideVueContextmenuName} from "/@/flow/utils";
// 引入组件
const VueContextMenu = defineAsyncComponent(() => import('../../components/contextmenu/index.vue'));
const {t} = useI18n();
const $emit = defineEmits(["initJsonFlowView"]);
const props = defineProps({
currSelect: {
type: Object,
default: {},
}
});
const data = reactive({
container: {
scaleShow: utils.mul(flowConfig.defaultStyle.containerScale.init, 100)
},
linkContMenuData: deepClone(flowConfig.contextMenu.linkView),
nodeContMenuData: deepClone(flowConfig.contextMenu.nodeView)
})
const methods = {
// 画布放大
enlargePaper() {
data.container.scaleShow = window._jfOperate.zoomOut();
},
// 画布缩小
narrowPaper() {
let zoomIn = window._jfOperate.zoomIn();
data.container.scaleShow = parseInt(zoomIn)
},
// 节点click事件
showNodeClickMenu(currSelect, e) {
// TODO 业务侧替换其他内容
methods.showNodeContMenu(currSelect, e)
},
// 节点hover事件
showNodeContMenu(currSelect, e) {
if (validateNull(currSelect)) {
currSelect = props.currSelect
}
if (Object.keys(currSelect).length === 0) return
// 计算节点信息
methods.updateNodeContMenuData(currSelect)
let event = window.event || e;
event.preventDefault();
hideVueContextmenuName()
let x = event.clientX;
let y = event.clientY;
data.nodeContMenuData.axis = {x, y};
},
updateNodeContMenuData(currSelect) {
let nodeNameBtnName = flowConfig.contextMenu.nodeView.menulists[0].btnName;
let text;
if (!currSelect.attributes.attrs.label) text = currSelect.attrs.label.text
else text = currSelect.attributes.attrs.label.text
data.nodeContMenuData.menulists[0].btnName = nodeNameBtnName + text
data.nodeContMenuData.menulists[1].btnName = currSelect.attributes.startTime
data.nodeContMenuData.menulists[2].btnName = currSelect.attributes.userRoleName
data.nodeContMenuData.menulists[3].btnName = currSelect.attributes.userName
data.nodeContMenuData.menulists[4].btnName = currSelect.attributes.remark
},
// 连接click事件
showLinkClickMenu(currSelect, e) {
// TODO 业务侧替换其他内容
methods.showLinkContMenu(currSelect, e)
},
// 连接线hover事件
showLinkContMenu(currSelect, e) {
if (validateNull(currSelect)) {
currSelect = props.currSelect
}
if (Object.keys(currSelect).length === 0) return
// 计算连线条件
let varKeyVal = currSelect.attributes.attrs.cdata.attrs.varKeyVal;
hideVueContextmenuName()
if (!varKeyVal) return
let btnName = flowConfig.contextMenu.linkView.menulists[0].btnName;
data.linkContMenuData.menulists[0].btnName = btnName + varKeyVal
let event = window.event || e;
event.preventDefault();
event.stopPropagation();
let x = event.clientX;
let y = event.clientY;
data.linkContMenuData.axis = {x, y};
}
}
onMounted(() => {
nextTick(() => {
$emit("initJsonFlowView");
})
});
// 暴露变量
defineExpose({
showNodeContMenu: methods.showNodeContMenu,
showNodeClickMenu: methods.showNodeClickMenu,
showLinkContMenu: methods.showLinkContMenu,
showLinkClickMenu: methods.showLinkClickMenu,
});
</script>
<style lang="scss">
@import "../assets/style/flow-area.scss";
@import "../assets/style/flow-paper.css";
</style>

View File

@@ -0,0 +1,384 @@
<template>
<div id="jfDragWrapPaper">
<div id="jfWrapPaper"
class="jf-wrap-paper"
>
<div id="jsonflowMainPaper" @dragover="methods.allowDrop" @drop="methods.drop"></div>
<div class="horizontal-line-x"></div>
<div class="vertical-line-y"></div>
</div>
<div id="minimapPaperView" class="jsonflow-navigator"></div>
<div class="container-scale">
<el-button icon="ZoomIn" circle size="small" type="default"
@click="methods.enlargePaper"></el-button>
<span>{{ data.container.scaleShow }}% </span>
<el-button icon="ZoomOut" circle size="small" type="default"
@click="methods.narrowPaper"></el-button>
</div>
<!-- 选择连接的节点 -->
<el-dialog
v-model="data.showSetConnectNode" v-if="data.showSetConnectNode"
top="20px"
width="50%"
title="请选择连接到的节点"
append-to-body>
<el-form label-position="left" class="flow-config-attr" label-width="170px">
<el-form-item label="连接到的节点">
<el-select class="input-attr" placeholder="请选择连接到的节点" style="width: 80%!important;"
@change="methods.doConnectNode"
v-model="data.toFlowNodeId"
filterable clearable>
<el-option
v-for="(item, index) in data.flowNodeIds"
:key="item.id"
:label="item.nodeName"
:value="item.id">
</el-option>
</el-select>
<el-tooltip placement="top">
<template #content>从当前节点连接到目标节点</template>
<el-icon style="margin-left: 10px"><QuestionFilled /></el-icon>
</el-tooltip>
</el-form-item>
</el-form>
</el-dialog>
<!-- 选择连线的起始节点 -->
<el-dialog
v-model="data.showLinkFlowNodeIds" v-if="data.showLinkFlowNodeIds"
top="20px"
width="50%"
:title="'请选择连接到的' + (data.modifyPointType === '0' ? '起点' : '终点')"
append-to-body>
<el-form label-position="left" class="flow-config-attr" label-width="170px">
<el-form-item label="起始节点(可修改)" v-if="data.modifyPointType ==='0'">
<el-select class="input-attr" style="width: 80%!important;"
v-model="data.fromFlowNodeId"
@change="methods.changeLinkFlowNodeIds('0')"
filterable clearable>
<el-option
v-for="(item, index) in data.fromFlowNodeIds"
:key="item.id"
:label="item.nodeName"
:value="item.id">
</el-option>
</el-select>
</el-form-item>
<el-form-item label="目标节点(可修改)" v-if="data.modifyPointType ==='1'">
<el-select class="input-attr" style="width: 80%!important;"
v-model="data.toFlowNodeId"
@change="methods.changeLinkFlowNodeIds('1')"
filterable clearable>
<el-option
v-for="(item, index) in data.toFlowNodeIds"
:key="item.id"
:label="item.nodeName"
:value="item.id">
</el-option>
</el-select>
</el-form-item>
</el-form>
</el-dialog>
<vue-context-menu
:contextMenuData="data.paperContMenuData"
@flowInfo="methods.flowInfo"
@paste="methods.paste"
>
</vue-context-menu>
<vue-context-menu
:contextMenuData="data.nodeContMenuData"
@setNodeAttr="methods.setNodeAttr('0')"
@copyNode="methods.copyNode"
@deleteNode="methods.deleteNode"
@setConnectNode="methods.setConnectNode"
>
</vue-context-menu>
<vue-context-menu
:contextMenuData="data.nodeConnectMenuData"
@setSerialNode="methods.setSerialNode"
@setSerialGate="methods.setSerialGate"
@setParallelNode="methods.setParallelNode"
@setParallelGate="methods.setParallelGate"
@setConnectNode="methods.setConnectNode"
>
</vue-context-menu>
<vue-context-menu
:contextMenuData="data.linkContMenuData"
@deleteLink="methods.deleteLink"
@setLinkAttr="methods.setNodeAttr('1')"
@modifySourceNode="methods.handleLinkFlowNodeIds('0')"
@modifyTargetNode="methods.handleLinkFlowNodeIds('1')"
>
</vue-context-menu>
</div>
</template>
<script setup lang="ts" name="FlowArea">
import {flowConfig} from "../config/flow-config";
import {utils} from "../utils/common";
import {useI18n} from "vue-i18n";
import {useMessage, useMessageBox} from "/@/hooks/message";
import {notifyLeft} from "/@/flow";
import {CommonNodeType, HighNodeType} from "../config/type";
import {hideVueContextmenuName} from "/@/flow/utils";
import {validateNull} from "/@/utils/validate";
import {changeLinkFlowNodeIds, handleLinkFlowNodeIds, validateNodeType} from "./index";
// 引入组件
const VueContextMenu = defineAsyncComponent(() => import('../../components/contextmenu/index.vue'));
const {t} = useI18n();
const {proxy} = getCurrentInstance();
const $message = useMessage();
const $emit = defineEmits(["removeEleTools", "showAttrConfig", "initJsonFlow"]);
const props = defineProps({
dragInfo: {
type: Object,
default: null,
},
currSelect: {
type: Object,
default: {},
}
});
const data = reactive({
container: {
scaleShow: utils.mul(flowConfig.defaultStyle.containerScale.init, 100)
},
paperContMenuData: flowConfig.contextMenu.container,
nodeContMenuData: flowConfig.contextMenu.node,
nodeConnectMenuData: flowConfig.contextMenu.nodeConnect,
linkContMenuData: flowConfig.contextMenu.link,
clipboard: {},
showSetConnectNode: false,
showLinkFlowNodeIds: false,
toFlowNodeId: null,
flowNodeIds: [],
modifyPointType: null,
fromFlowNodeId: null,
fromFlowNodeIds: [],
toFlowNodeIds: []
})
const methods = {
allowDrop(e) {
e.preventDefault();
},
drop(e) {
// 增加节点
window._jfOperate.dropNewNode(e, props.dragInfo, true);
},
// 画布放大
enlargePaper() {
data.container.scaleShow = window._jfOperate.zoomOut();
},
// 画布缩小
narrowPaper() {
let zoomIn = window._jfOperate.zoomIn();
data.container.scaleShow = parseInt(zoomIn)
},
// 画布右健
showPaperContMenu(e) {
let event = window.event || e;
event.preventDefault();
hideVueContextmenuName()
let x = event.clientX;
let y = event.clientY;
data.paperContMenuData.axis = {x, y};
},
// 流程图信息
flowInfo() {
$message.info(
"当前流程图中有 " +
window._jfGraph.getElements().length +
" 个节点,有 " +
window._jfGraph.getLinks().length +
" 条连线。"
);
},
handleFlowNodeIds() {
data.flowNodeIds = []
let models = window._jfGraph.getElements();
if (validateNull(models)) return
models.forEach(each => {
if (!validateNodeType(each)) return
if (props.currSelect.id !== each.id) {
let id = each.id
let nodeName = each.attributes.attrs.label.text + "ID:" + id + ""
data.flowNodeIds.push({id, nodeName})
}
})
},
// 粘贴
paste() {
let e = window.event;
let b = Object.keys(data.clipboard).length === 0;
if (b) {
$message.info("请将鼠标放节点上, 右键菜单复制节点");
hideVueContextmenuName()
return
}
let newNode = data.clipboard.clone()
window._jfOperate.pasteNode(newNode, e);
data.clipboard = {}
hideVueContextmenuName()
},
// 节点右键
showNodeContMenu(e) {
let event = window.event || e;
event.preventDefault();
hideVueContextmenuName()
let x = event.clientX;
let y = event.clientY;
data.nodeContMenuData.axis = {x, y};
},
// 复制节点
copyNode() {
data.clipboard = {};
if (methods.validateCurrSelect("0")) {
return
}
data.clipboard = props.currSelect;
hideVueContextmenuName()
},
// 删除节点
deleteNode() {
if (methods.validateCurrSelect("0")) {
return
}
props.currSelect.remove()
$emit("removeEleTools");
},
setNodeAttr(type) {
if (methods.validateCurrSelect(type)) {
return
}
$emit("showAttrConfig", true);
},
// 连接线右键
showLinkContMenu(e) {
let event = window.event || e;
event.preventDefault();
event.stopPropagation();
hideVueContextmenuName()
let x = event.clientX;
let y = event.clientY;
data.linkContMenuData.axis = {x, y};
},
// 删除线
deleteLink() {
if (methods.validateCurrSelect("1")) {
return
}
props.currSelect.remove()
$emit("removeEleTools");
},
validateCurrSelect(type) {
let b = Object.keys(props.currSelect).length === 0;
if (b !== true) return false
if (type === '0') {
notifyLeft('请先选择节点', 'warning')
} else {
notifyLeft('请先移动到连线上方', 'warning')
}
return true
},
// 节点连接右键
showNodeConnectMenu(params, e) {
let event = window.event || e;
hideVueContextmenuName()
let x = event.clientX;
let y = event.clientY;
data.nodeConnectMenuData.axis = {x, y};
data.nodeConnectMenuData.params = params;
},
setSerialNode() {
methods.connectAction({belongTo: "commonNodes", type: CommonNodeType.SERIAL})
},
setParallelNode() {
methods.connectAction({belongTo: "commonNodes", type: CommonNodeType.PARALLEL})
},
setSerialGate() {
methods.connectAction({belongTo: "highNodes", type: CommonNodeType.SERIAL})
},
setParallelGate() {
methods.connectAction({belongTo: "highNodes", type: CommonNodeType.PARALLEL})
},
setConnectNode() {
if (methods.hideValidateCurrSelect("0")) {
return
}
methods.handleFlowNodeIds()
data.toFlowNodeId = null
data.showSetConnectNode = true
},
doConnectNode(val) {
if (methods.hideValidateCurrSelect("0")) {
return
}
let simpleMode = window._flowConfig.globalConfig.isSimpleMode;
if (simpleMode !== '1') {
data.nodeConnectMenuData.params = {view: {model: props.currSelect}}
}
useMessageBox()
.confirm('是否确认连接到当前选中的节点?')
.then(() => {
let params = data.nodeConnectMenuData.params;
params.toFlowNodeId = val
window._jfOperate.doConnectNode(params)
data.showSetConnectNode = false
if (simpleMode !== '1') {
notifyLeft('专业模式不会自动调整连线轨迹,有必要时请手动调整', 'warning', 3000)
}
})
},
connectAction(dragInfo) {
if (methods.hideValidateCurrSelect("0")) {
return
}
let params = data.nodeConnectMenuData.params;
window._jfOperate.connectAction(params, dragInfo)
},
hideValidateCurrSelect(type) {
hideVueContextmenuName()
return methods.validateCurrSelect(type);
},
changeLinkFlowNodeIds(type) {
if (methods.hideValidateCurrSelect("1")) {
return
}
changeLinkFlowNodeIds(data, props)
data.showLinkFlowNodeIds = false
},
handleLinkFlowNodeIds(type) {
if (methods.hideValidateCurrSelect("1")) {
return
}
data.modifyPointType = type
handleLinkFlowNodeIds(data, props)
data.showLinkFlowNodeIds = true
},
}
onMounted(() => {
nextTick(() => {
$emit("initJsonFlow");
})
});
// 暴露变量
defineExpose({
showPaperContMenu: methods.showPaperContMenu,
showNodeContMenu: methods.showNodeContMenu,
showNodeConnectMenu: methods.showNodeConnectMenu,
showLinkContMenu: methods.showLinkContMenu,
});
</script>
<style lang="scss">
@import "../assets/style/flow-area.scss";
@import "../assets/style/flow-paper.css";
</style>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,411 @@
<template>
<div>
<el-form label-position="left" class="flow-config-attr" label-width="160px">
<el-form-item label="监听类">
<el-input class="input-attr" :placeholder="props.clazzPlaceholder" v-model="data.clazz.clazz" clearable></el-input>
</el-form-item>
<el-form-item label="Http请求地址">
<el-input class="input-attr" placeholder="可输入全路径或相对路径" v-model="data.clazz.httpUrl"
@blur="methods.handleHttpUrlBlur" clearable>
<template #prepend>
<el-select v-model="data.clazz.httpMethod">
<el-option v-for="(item, index) in DIC_PROP.HTTP_METHODS" :key="index" :label="item.label" :value="item.value"></el-option>
</el-select>
</template>
</el-input>
<el-tooltip content="点击可设置Http请求的参数信息" placement="bottom">
<el-button type="primary" size="small" round style="margin-left: 10px"
@click="methods.openHttpUrlParams(true)">
参数
</el-button>
</el-tooltip>
</el-form-item>
<el-form-item label="触发时机">
<el-select class="input-attr"
v-model="data.clazz.methods" multiple
clearable placeholder="多个方法名称, 英文逗号分隔, 顺序从左到右">
<el-option
v-for="(item, index) in props.addType === '0' && props.currSelect.attributes.attrs.cdata.attrs.isAutoAudit === '1' ?
props.flowMethods.slice(3) : props.flowMethods"
:key="item.value"
:label="item.label"
:value="item.value">
</el-option>
</el-select>
</el-form-item>
<el-divider v-if="!data.moreInter">
<el-button type="primary" link @click="methods.showMoreSetting()"
style="margin-left: 10px; font-size: 14px">
{{ data.moreInter ? '隐藏更多' : '更多配置' }}
</el-button>
</el-divider>
<template v-if="data.moreInter">
<div style="margin: 10px 13px">
<span style="color: red;font-size: 14px">注: 监听事件允许设置生效条件,用于控制在某种条件下才会执行</span>
</div>
<el-form-item label="条件模式">
<el-radio-group style="width: 313px" @change="methods.handleClazzValType" :disabled="data.existData"
v-model="data.clazz.valType">
<el-radio v-for="(item, index) in DIC_PROP.VAL_TYPE.slice(2, 5)" :key="index" :label="item.value" style="width: 75px">
{{ item.label }}
</el-radio>
</el-radio-group>
<el-tooltip placement="top">
<template #content>若无法切换模式,请先清空监听事件列表</template>
<el-icon><QuestionFilled /></el-icon>
</el-tooltip>
</el-form-item>
<el-table :data="data.tableFieldData" v-if="data.clazz.valType === '0'"
border style="width: 100%; margin-bottom: 10px" max-height="500">
<el-table-column prop="varKeyVal" :label="t('flowClazz.varKeyVal')" show-overflow-tooltip>
<template #default="scope">
<el-select v-model="data.clazz.varKeyVal" @change="methods.handleVarKeyVal"
clearable
filterable>
<el-option-group
v-for="(group, index) in data.allFieldPerms"
:key="index"
:label="group.label">
<el-option
v-for="(item, index) in group.options"
:disabled="item.prop.indexOf('_define_') !== -1"
:key="index"
:label="item.label"
:value="item.prop">
</el-option>
</el-option-group>
</el-select>
</template>
</el-table-column>
<el-table-column prop="operator" :label="t('flowClazz.operator')" show-overflow-tooltip>
<template #default="scope">
<el-select v-model="data.clazz.operator"
clearable>
<el-option
v-for="(item, index) in data.varValOperator"
:key="item.value"
:label="item.label"
:value="item.value">
</el-option>
</el-select>
</template>
</el-table-column>
<el-table-column prop="varVal" :label="t('flowClazz.varVal')" show-overflow-tooltip>
<template #default="scope">
<el-input v-model="data.clazz.varVal" clearable/>
</template>
</el-table-column>
</el-table>
<el-form-item label="SpEL表达式" v-if="data.clazz.valType === '1'">
<el-input class="input-attr" v-model="data.clazz.varKeyVal" clearable @blur="methods.handleClazzVarKeyVal"/>
<el-tooltip placement="top">
<template #content>{{ PROP_CONST.TEXT_DESC.condSpELExplain }}</template>
<el-icon style="margin-left: 10px"><QuestionFilled /></el-icon>
</el-tooltip>
</el-form-item>
<el-form-item label="函数表达式" v-if="data.clazz.valType === '2'">
<el-input class="input-attr" v-model="data.clazz.varKeyVal" placeholder="请输入函数表达式" clearable @blur="methods.handleClazzVarKeyVal"/>
<el-tooltip placement="top">
<template #content>{{ PROP_CONST.TEXT_DESC.condMethodExplain1 }}
<br />{{ PROP_CONST.TEXT_DESC.condMethodExplain2 }}
<br />{{ PROP_CONST.TEXT_DESC.condMethodExplain3 }}
<br />{{ PROP_CONST.TEXT_DESC.condMethodExplain4 }}
</template>
<el-icon style="margin-left: 10px"><QuestionFilled /></el-icon>
</el-tooltip>
</el-form-item>
<el-form-item label="监听备注">
<el-input class="input-attr" type="textarea" v-model="data.clazz.remark"></el-input>
</el-form-item>
<el-form-item label="监听排序">
<el-input-number class="input-attr" v-model="data.clazz.sort" :min="1"></el-input-number>
</el-form-item>
</template>
<el-button type="primary" round style="margin-left: 185px; margin-top: 30px; margin-bottom: 30px; width: 200px" @click="methods.addFlowNodeClazz(props.addType)">
{{ data.clazz.isSelectedRow ? '修改完成': '添加事件' }}
</el-button>
<el-divider>已添加监听事件列表(点击行可再次修改)</el-divider>
<el-empty description="监听事件列表为空" style="margin: 10px 230px" v-if="!data.existData">
</el-empty>
<el-table :data="data.tableData" border style="width: 100%" max-height="500" v-else
highlight-current-row @current-change="methods.handleCurrentChange" ref="tableDataRef">
<el-table-column type="index" label="操作" width="55">
<template #default="scope">
<el-button icon="Minus" size="small" type="danger" circle @click="methods.handleClazzDelete(scope.$index, scope.row, props.addType)"></el-button>
</template>
</el-table-column>
<el-table-column prop="clazz" :label="t('flowClazz.clazz')" width="130" show-overflow-tooltip/>
<el-table-column prop="httpUrl" :label="t('flowClazz.httpUrl')" width="130" show-overflow-tooltip>
</el-table-column>
<el-table-column prop="methods" label="触发时机" width="130" show-overflow-tooltip>
<template #default="scope">
<dict-tag :options="props.flowMethods" :value="scope.row.methods"></dict-tag>
</template>
</el-table-column>
<el-table-column prop="varKeyVal" label="函数表达式" width="130" show-overflow-tooltip v-if="data.clazz.valType === '2'"/>
<el-table-column prop="varKeyVal" label="SpEL表达式" width="130" show-overflow-tooltip v-if="data.clazz.valType === '1'"/>
<el-table-column prop="varKeyVal" :label="t('flowClazz.varKeyVal')" width="130" show-overflow-tooltip v-if="data.clazz.valType === '0'">
<template #default="scope">
<convert-group-name :options="data.allFieldPerms" :value="scope.row.varKeyVal"
:valueKey="'prop'" :showKey="'label'"></convert-group-name>
</template>
</el-table-column>
<el-table-column prop="operator" :label="t('flowClazz.operator')" width="100" show-overflow-tooltip v-if="data.clazz.valType === '0'">
<template #default="scope">
<dict-tag :options="DIC_PROP.OPERATOR" :value="scope.row.operator"></dict-tag>
</template>
</el-table-column>
<el-table-column prop="varVal" :label="t('flowClazz.varVal')" width="100" show-overflow-tooltip v-if="data.clazz.valType === '0'"/>
<el-table-column prop="remark" :label="t('flowClazz.remark')" width="70" show-overflow-tooltip/>
<el-table-column prop="sort" :label="t('flowClazz.sort')" width="70" show-overflow-tooltip/>
</el-table>
</el-form>
<el-drawer
class="flow-attr-drawer"
title="Http请求的参数信息"
direction="rtl"
append-to-body
:size="630" v-if="data.httpUrlParamsVisible"
v-model="data.httpUrlParamsVisible">
<el-form label-position="left" class="flow-attr flow-param-attr" label-width="150px">
<flow-http-param ref="flowHttpParam" :currFlowForm="props.currFlowForm" :httpParam="data.clazz"
:flowData="props.flowData"></flow-http-param>
</el-form>
</el-drawer>
</div>
</template>
<script setup lang="ts" name="FlowClazz">
import {useI18n} from "vue-i18n";
import {useMessage} from "/@/hooks/message";
import {rule, validateNull} from "/@/utils/validate";
import {deepClone} from "/@/utils/other";
import {DIC_PROP} from "/@/flow/support/dict-prop";
import {buildSysFieldsFormOption} from "../../utils/form-perm";
const { proxy } = getCurrentInstance();
const FlowHttpParam = defineAsyncComponent(() => import('./flow-http-param.vue'));
const {t} = useI18n();
const $message = useMessage();
const props = defineProps({
currFlowForm: {
type: Object,
default: null,
},
currSelect: {
type: Object,
default: null,
},
flowData: {
type: Object,
default: null,
},
clazzPlaceholder: {
type: String,
default: '',
},
flowMethods: {
type: Array,
default: [],
},
addType: {
type: String,
default: '',
}
});
const clazzData = {
clazz: null,
httpUrl: null,
httpMethod: 'GET',
httpParams: null,
methods: null,
valType: null,
varKeyVal: null,
operator: null,
varVal: null,
remark: null,
sort: 1,
}
const data = reactive({
tableFieldData: [],
allFieldPerms: [],
formFieldPerms: [],
clazz: deepClone(clazzData),
httpUrlParamsVisible: false,
moreInter: false,
existData: false,
tableData: []
})
onMounted(() => {
methods.changeTabPane(props.currSelect)
})
const methods = {
openHttpUrlParams(bool) {
if (!data.clazz.httpUrl) {
$message.warning("请先输入【Http请求地址】")
return
}
data.httpUrlParamsVisible = bool
},
handleHttpUrlBlur() {
if (!data.clazz.httpUrl) {
data.clazz.httpParams = []
return;
}
},
handleCurrentChange(row) {
if (!row) return
data.clazz = row
data.clazz.isSelectedRow = true
},
handleVarKeyVal(varKeyVal) {
let dots = varKeyVal.split('.').length - 1;
if (dots === 2) {
data.varValOperator = DIC_PROP.OPERATOR.slice(6)
} else {
data.varValOperator = DIC_PROP.OPERATOR
}
},
onAddItem(){
if (validateNull(data.formFieldPerms)) {
buildSysFieldsFormOption(data, props, $message)
}
if (data.tableFieldData.length > 0) return
let obj = {varKeyVal: '', operator: '', varVal: ''};
data.tableFieldData.push(obj);
},
addFlowNodeClazz(type) {
let clazzes = type === '0' ? props.currSelect.attributes.attrs.cdata.attrs.clazzes : props.flowData.attrs.clazzes;
if (!data.clazz.clazz && !data.clazz.httpUrl) {
$message.warning("请填写 监听类 或者 Http请求地址")
return
}
if (validateNull(data.clazz.methods)) {
$message.warning("触发时机不能为空")
return
}
if (!validateNull(clazzes) && !data.clazz.isSelectedRow) {
if (data.clazz.clazz) {
let b = methods.validateClazzHttpUrl(clazzes, 'clazz', '监听类');
if (b) return;
} else {
let b = methods.validateClazzHttpUrl(clazzes, 'httpUrl', 'Http请求地址');
if (b) return;
}
}
if (data.clazz.clazz && data.clazz.httpUrl) {
$message.error("监听类 与 Http请求地址不能同时存在")
return;
}
if (data.clazz.isSelectedRow) {
data.clazz.isSelectedRow = false
proxy.$refs.tableDataRef.setCurrentRow(null)
data.clazz = deepClone(clazzData);
} else {
let clazz = deepClone(data.clazz);
if (type === '0') props.currSelect.attributes.attrs.cdata.attrs.clazzes.unshift(clazz)
else props.flowData.attrs.clazzes.unshift(clazz)
methods.validateClazzData()
}
},
validateClazzHttpUrl(clazzes, key, errMsg){
let find = clazzes.find(f => f[key] === data.clazz[key]);
if (find) {
$message.warning("请勿重复添加 " + errMsg)
return true
}
return false
},
handleClazzDelete(index: number, row: any, type: string){
if (type === '0') props.currSelect.attributes.attrs.cdata.attrs.clazzes.splice(index, 1)
else props.flowData.attrs.clazzes.splice(index, 1)
methods.validateClazzData()
},
handleClazzValType(type?) {
if (!type) {
type = methods.initCurrVarKeyVal()
}
if (type === DIC_PROP.VAL_TYPE[2].value) {
data.varValOperator = DIC_PROP.OPERATOR
methods.onAddItem()
} else {
data.varValOperator = []
}
data.clazz.varKeyVal = null
data.clazz.operator = null
data.clazz.varVal = null
},
handleClazzVarKeyVal() {
let val = data.clazz.varKeyVal
if (!val) return
let valType = data.clazz.valType;
if (valType === DIC_PROP.VAL_TYPE[3].value && val.indexOf("#") === -1) {
data.clazz.varKeyVal = null
$message.warning("当选择SpEL模式时, SpEL表达式必须符合SpEL格式")
} else if (valType === DIC_PROP.VAL_TYPE[4].value && val.indexOf("#") === -1) {
data.clazz.varKeyVal = null
$message.warning("当选择专业模式时, 函数表达式必须符合规定的格式")
}
},
showMoreSetting() {
data.moreInter = !data.moreInter
},
validateClazzData() {
let clazzes = props.addType === '0' ? props.currSelect.attributes.attrs.cdata.attrs.clazzes : props.flowData.attrs.clazzes;
if (!clazzes) {
if (props.addType === '0') props.currSelect.attributes.attrs.cdata.attrs.clazzes = []
else props.flowData.attrs.clazzes = []
}
data.tableData.splice(0, data.tableData.length);
if (props.addType === '0') {
props.currSelect.attributes.attrs.cdata.attrs.clazzes.forEach(each => data.tableData.push(each))
} else {
props.flowData.attrs.clazzes.forEach(each => data.tableData.push(each))
}
data.existData = !validateNull(data.tableData)
},
initCurrVarKeyVal() {
if (data.tableData.length > 0) {
data.clazz.valType = data.tableData[0].valType
}
data.clazz.methods = []
return data.clazz.valType
},
changeTabPane(val) {
methods.validateClazzData()
methods.handleClazzValType()
}
}
// 监听双向绑定
watch(
() => props.currSelect,
(val) => {
if (Object.keys(val).length === 0) {
return
}
methods.changeTabPane(val)
}
);
</script>
<style lang="scss">
@import '../assets/style/flow-attr.scss';
</style>

View File

@@ -0,0 +1,527 @@
<template>
<el-form-item label="起始节点(可修改)">
<el-select class="input-attr"
v-model="data.fromFlowNodeId"
@change="methods.changeLinkFlowNodeIds('0')"
filterable clearable>
<el-option
v-for="(item, index) in data.fromFlowNodeIds"
:key="item.id"
:label="item.nodeName"
:value="item.id">
</el-option>
</el-select>
</el-form-item>
<el-form-item label="目标节点(可修改)">
<el-select class="input-attr"
v-model="data.toFlowNodeId"
@change="methods.changeLinkFlowNodeIds('1')"
filterable clearable>
<el-option
v-for="(item, index) in data.toFlowNodeIds"
:key="item.id"
:label="item.nodeName"
:value="item.id">
</el-option>
</el-select>
</el-form-item>
<el-form-item label="条件组关系">
<el-switch
v-model="data.cond.groupsType" @change="methods.changeGroupsType"
active-value="0"
inactive-value="1"
inactive-text=""
active-text="">
</el-switch>
</el-form-item>
<el-form-item label="条件模式">
<el-radio-group @change="methods.handleCondValType" :disabled="data.existData"
v-model="data.cond.valType">
<el-radio v-for="(item, index) in DIC_PROP.VAL_TYPE.slice(2, 6)" :key="index" :label="item.value"
style="width: 56px">
{{ item.label }}
</el-radio>
</el-radio-group>
<el-tooltip placement="top">
<template #content>若无法切换模式请先清空条件组列表</template>
<el-icon style="margin-left: 30px">
<QuestionFilled/>
</el-icon>
</el-tooltip>
</el-form-item>
<el-divider>组内条件配置</el-divider>
<template v-if="data.cond.valType === '0'">
<el-form-item label="组内条件关系">
<el-switch
v-model="data.cond.groupType"
active-value="0"
inactive-value="1"
inactive-text=""
active-text="">
</el-switch>
</el-form-item>
<el-table :data="data.cond.condGroup"
border style="width: 100%; margin-bottom: 10px" max-height="500">
<el-table-column type="index" label="操作" width="80">
<template #header>
<el-button icon="Plus" size="small" type="primary" circle @click="methods.onAddItem(true)"></el-button>
</template>
<template #default="scope">
<el-button icon="Minus" size="small" type="danger" circle @click="methods.handleCondDelete(scope.$index, scope.row)"></el-button>
</template>
</el-table-column>
<el-table-column prop="varKeyVal" :label="t('flowClazz.varKeyVal')" show-overflow-tooltip>
<template #default="scope">
<el-select v-model="scope.row.varKeyVal" @change="methods.handleVarKeyVal(scope.row)"
clearable
filterable>
<el-option-group
v-for="(group, index) in data.allFieldPerms"
:key="index"
:label="group.label">
<el-option
v-for="(item, index) in group.options"
:disabled="item.prop.indexOf('_define_') !== -1"
:key="index"
:label="item.label"
:value="item.prop">
</el-option>
</el-option-group>
</el-select>
</template>
</el-table-column>
<el-table-column prop="operator" :label="t('flowClazz.operator')" show-overflow-tooltip>
<template #default="scope">
<el-select v-model="scope.row.operator" @change="methods.handleVarKeyVal(scope.row)"
clearable>
<el-option
v-for="(item, index) in data.varValOperator"
:key="item.value"
:label="item.label"
:value="item.value">
</el-option>
</el-select>
</template>
</el-table-column>
<el-table-column prop="varVal" :label="t('flowClazz.varVal')" show-overflow-tooltip>
<template #default="scope">
<el-input v-model="scope.row.varVal" clearable/>
</template>
</el-table-column>
</el-table>
</template>
<el-form-item label="SpEL表达式" v-if="data.cond.valType === '1'">
<el-input class="input-attr" v-model="data.cond.varKeyVal" placeholder="请输入SpEL表达式" clearable @blur="methods.handleCondVarKeyVal"/>
<el-tooltip placement="top">
<template #content>{{ PROP_CONST.TEXT_DESC.condSpELExplain }}</template>
<el-icon style="margin-left: 10px">
<QuestionFilled/>
</el-icon>
</el-tooltip>
</el-form-item>
<el-form-item label="函数表达式" v-if="data.cond.valType === '2'">
<el-input class="input-attr" v-model="data.cond.varKeyVal" placeholder="请输入函数表达式" clearable @blur="methods.handleCondVarKeyVal"/>
<el-tooltip placement="top">
<template #content>{{ PROP_CONST.TEXT_DESC.condMethodExplain1 }}
<br />{{ PROP_CONST.TEXT_DESC.condMethodExplain2 }}
<br />{{ PROP_CONST.TEXT_DESC.condMethodExplain3 }}
<br />{{ PROP_CONST.TEXT_DESC.condMethodExplain4 }}
</template>
<el-icon style="margin-left: 10px">
<QuestionFilled/>
</el-icon>
</el-tooltip>
</el-form-item>
<div v-if="data.cond.valType === '2'" style="margin: 10px 13px">
<span style="color: #409EFF;font-size: 14px">: 当前函数表达式的返回值为字符串 1 ( 满足 ) 0 ( 不满足 )</span>
</div>
<template v-if="data.cond.valType === '3'">
<el-form-item label="Http请求地址">
<el-input class="input-attr" placeholder="可输入全路径或相对路径" v-model="data.cond.varKeyVal"
@blur="methods.handleCondVarKeyVal" clearable>
<template #prepend>
<el-select v-model="data.cond.httpMethod">
<el-option v-for="(item, index) in DIC_PROP.HTTP_METHODS" :key="index" :label="item.label" :value="item.value"></el-option>
</el-select>
</template>
</el-input>
<el-tooltip content="点击可设置Http请求的参数信息" placement="bottom">
<el-button type="primary" size="small" round style="margin-left: 10px"
@click="methods.openHttpUrlParams()">
{{ data.httpUrlParamsVisible ? '清空' : '参数' }}
</el-button>
</el-tooltip>
</el-form-item>
<flow-http-param ref="flowHttpParam" :currFlowForm="props.currFlowForm" :flowData="props.flowData"
:httpParam="props.currSelect.attributes.attrs.cdata.attrs"
:httpParamType="DIC_PROP.PARAM_RULE_TYPE[1].value"
v-if="data.httpUrlParamsVisible">
</flow-http-param>
</template>
<template v-if="data.cond.valType === '0'">
<el-button type="primary" round style="margin-left: 185px; margin-top: 30px; margin-bottom: 30px; width: 200px"
@click="methods.addFlowNodeCondGroup()">
{{ data.oldCurrentRow ? '修改完成': '添加条件组' }}
</el-button>
<el-divider>已添加条件组列表(点击行可再次修改)</el-divider>
<el-empty description="条件组列表为空" style="margin: 10px 230px" v-if="!data.existData">
</el-empty>
<template v-for="(item, index) in data.condGroups" v-else
:key="index">
<el-collapse v-model="data.collapse">
<el-collapse-item :name="index">
<template #title>
{{ '条件组 ' + (index + 1) }}
<el-icon style="margin-left: 10px" @click="methods.handleCondGroupDelete(index)">
<Delete />
</el-icon>
</template>
<el-form-item label="组内条件关系">
<el-switch
v-model="item.groupType" @change="methods.changeGroupType(item, index)"
active-value="0"
inactive-value="1"
inactive-text=""
active-text="">
</el-switch>
</el-form-item>
<el-table :data="item.condGroup" border style="width: 100%" max-height="500"
highlight-current-row @current-change="methods.handleCurrentChange" :ref="'tableDataRef' + index">
<el-table-column type="index" label="操作" width="80">
<template #default="scope">
<el-button icon="Minus" size="small" type="danger" circle
@click="methods.handleSingleCondDelete(scope.$index, scope.row, index)"></el-button>
</template>
</el-table-column>
<el-table-column prop="varKeyVal" :label="t('flowClazz.varKeyVal')" show-overflow-tooltip>
<template #default="scope">
<convert-group-name :options="data.allFieldPerms"
:value="scope.row.varKeyVal"
:valueKey="'prop'" :showKey="'label'"></convert-group-name>
</template>
</el-table-column>
<el-table-column prop="operator" :label="t('flowClazz.operator')" show-overflow-tooltip>
<template #default="scope">
<dict-tag :options="DIC_PROP.OPERATOR" :value="scope.row.operator"></dict-tag>
</template>
</el-table-column>
<el-table-column prop="varVal" :label="t('flowClazz.varVal')" show-overflow-tooltip/>
</el-table>
</el-collapse-item>
</el-collapse>
</template>
</template>
</template>
<script setup lang="ts" name="FlowConRule">
import {useI18n} from "vue-i18n";
import {useMessage} from "/@/hooks/message";
import {rule, validateNull} from "/@/utils/validate";
import {deepClone} from "/@/utils/other";
import {DIC_PROP} from "/@/flow/support/dict-prop";
import {buildSysFieldsFormOption} from "../../utils/form-perm";
import {PROP_CONST} from "../../support/prop-const";
import {changeLinkFlowNodeIds, handleLinkFlowNodeIds} from "./index";
const {proxy} = getCurrentInstance();
const FlowHttpParam = defineAsyncComponent(() => import('./flow-http-param.vue'));
const {t} = useI18n();
const $message = useMessage();
const $emit = defineEmits(["hideAttrConfig"]);
const props = defineProps({
currFlowForm: {
type: Object,
default: null,
},
currSelect: {
type: Object,
default: null,
},
flowData: {
type: Object,
default: null,
},
attrConfigVisible: null
});
const condData = {
httpMethod: 'GET',
groupsType: '0',
condGroup: [],
groupType: '0',
valType: '0',
varKeyVal: null,
operator: null,
varVal: null,
}
const data = reactive({
collapse: [0],
allFieldPerms: [],
formFieldPerms: [],
cond: deepClone(condData),
httpUrlParamsVisible: false,
oldCurrentRow: null,
condGroups: [],
existData: false,
modifyPointType: null,
fromFlowNodeId: null,
fromFlowNodeIds: [],
toFlowNodeId: null,
toFlowNodeIds: []
})
onMounted(() => {
methods.changeTabPane(props.currSelect)
})
const methods = {
openHttpUrlParams() {
if (!data.cond.varKeyVal) {
$message.warning("请先输入【Http请求地址】")
return
}
if (data.httpUrlParamsVisible) {
props.currSelect.attributes.attrs.cdata.attrs.httpParams = []
data.httpUrlParamsVisible = false
} else {
data.httpUrlParamsVisible = true
}
},
handleCurrentChange(row) {
if (!row) return
let condGroupsRow;
for (let i = 0; i < data.condGroups.length; i++) {
let index = data.condGroups[i].condGroup.indexOf(row);
if (index !== -1) {
condGroupsRow = data.condGroups[i];
}
}
// 先清空之前选中的其他条件组
if (data.oldCurrentRow !== condGroupsRow) {
let oldIndex = data.condGroups.indexOf(data.oldCurrentRow);
if (oldIndex !== -1) {
proxy.$refs['tableDataRef' + oldIndex][0].setCurrentRow(null)
}
}
data.cond = condGroupsRow
data.oldCurrentRow = condGroupsRow
},
handleVarKeyVal(row) {
let dots = row.varKeyVal.split('.').length - 1;
if (dots === 2) {
let find = DIC_PROP.OPERATOR.slice(6).find(f => f.value === row.operator);
if (!find) {
$message.warning("子表单的字段只能选择包含或不包含")
row.operator = null
}
}
},
changeGroupsType(groupsType) {
let condGroups = props.currSelect.attributes.attrs.cdata.attrs.condGroups;
if (validateNull(condGroups)) return
props.currSelect.attributes.attrs.cdata.attrs.condGroups.forEach(each => {
each.groupsType = groupsType
})
methods.validateCondData()
},
changeGroupType(item, index) {
let condGroups = props.currSelect.attributes.attrs.cdata.attrs.condGroups;
if (validateNull(condGroups)) return
props.currSelect.attributes.attrs.cdata.attrs.condGroups[index].groupType = item.groupType
methods.validateCondData()
},
onAddItem(isAdd) {
if (validateNull(data.formFieldPerms)) {
buildSysFieldsFormOption(data, props, $message)
}
if (data.cond.condGroup.length > 0) {
let find = data.cond.condGroup.find(f => !f.varKeyVal || !f.operator || !f.varVal);
if (find) {
let b = !find.varKeyVal || !find.operator || !find.varVal;
if (isAdd && b) {
$message.warning("请先填写 表单字段 或 运算符 或 值")
return
}
}
if (!isAdd) data.cond.condGroup.splice(0, data.cond.condGroup.length);
}
let obj = {varKeyVal: '', operator: '', varVal: ''};
data.cond.condGroup.push(obj);
},
addFlowNodeCondGroup() {
if (validateNull(data.cond.condGroup)) {
$message.warning("请先添加组内条件")
return
}
let valType = data.cond.valType;
let find = data.cond.condGroup.find(f => !f.varKeyVal || !f.operator || !f.varVal);
if (find) {
if (valType === DIC_PROP.VAL_TYPE[2].value) {
if (!find.varKeyVal || !find.operator || !find.varVal) {
$message.warning("表单字段 或 运算符 或 值 不能为空")
return
}
}
}
if (data.oldCurrentRow) {
// 先清空之前选中
let index = data.condGroups.indexOf(data.cond);
if (index !== -1) {
proxy.$refs['tableDataRef' + index][0].setCurrentRow(null)
}
data.oldCurrentRow = null
data.cond = deepClone(condData);
} else {
let cond = deepClone(data.cond);
props.currSelect.attributes.attrs.cdata.attrs.condGroups.unshift(cond)
methods.validateCondData()
methods.updateCondVarKeyVal(true)
}
},
handleCondGroupDelete(index: number) {
props.currSelect.attributes.attrs.cdata.attrs.condGroups.splice(index, 1)
methods.validateCondData()
},
handleSingleCondDelete(index: number, row: any, groupIndex) {
props.currSelect.attributes.attrs.cdata.attrs.condGroups[groupIndex].condGroup.splice(index, 1)
if (validateNull(props.currSelect.attributes.attrs.cdata.attrs.condGroups[groupIndex].condGroup)) {
methods.handleCondGroupDelete(groupIndex)
}
methods.validateCondData()
},
handleCondDelete(index: number, row: any) {
data.cond.condGroup.splice(index, 1)
},
handleCondValType(type?) {
if (type) {
data.cond.varKeyVal = null
methods.updateCondVarKeyVal(false)
}
if (!type) {
type = methods.initCurrVarKeyVal()
}
if (type === DIC_PROP.VAL_TYPE[2].value) {
data.varValOperator = DIC_PROP.OPERATOR
methods.onAddItem(false)
} else {
data.varValOperator = []
}
data.cond.operator = null
data.cond.varVal = null
},
handleCondVarKeyVal() {
let val = data.cond.varKeyVal
let valType = data.cond.valType;
if (!val) {
methods.updateCondVarKeyVal(false)
return
}
if (valType === DIC_PROP.VAL_TYPE[2].value) return
if (valType === DIC_PROP.VAL_TYPE[3].value && val.indexOf("#") === -1) {
data.cond.varKeyVal = null
$message.warning("当选择SpEL模式时, SpEL表达式必须符合SpEL格式")
return;
} else if (valType === DIC_PROP.VAL_TYPE[4].value && val.indexOf("#") === -1) {
data.cond.varKeyVal = null
$message.warning("当选择专业模式时, 函数表达式必须符合规定的格式")
return;
}
methods.updateCondVarKeyVal(true)
},
updateCondVarKeyVal(isSave) {
props.currSelect.attributes.attrs.cdata.attrs.valType = data.cond.valType
if (isSave) {
if (data.cond.valType === DIC_PROP.VAL_TYPE[2].value) {
props.currSelect.attributes.attrs.cdata.attrs.varKeyVal = PROP_CONST.VAR_KEY_VAL.link
} else {
props.currSelect.attributes.attrs.cdata.attrs.varKeyVal = data.cond.varKeyVal
}
props.currSelect.attributes.attrs.cdata.attrs.httpMethod = data.cond.httpMethod
} else {
props.currSelect.attributes.attrs.cdata.attrs.varKeyVal = null
props.currSelect.attributes.attrs.cdata.attrs.varKeyValName = null
props.currSelect.attributes.attrs.cdata.attrs.condGroups = []
props.currSelect.attributes.attrs.cdata.attrs.httpParams = []
props.currSelect.attributes.attrs.cdata.attrs.httpMethod = null
data.httpUrlParamsVisible = false
}
},
validateCondData() {
let condGroups = props.currSelect.attributes.attrs.cdata.attrs.condGroups;
if (!condGroups) {
props.currSelect.attributes.attrs.cdata.attrs.condGroups = []
}
data.condGroups.splice(0, data.condGroups.length);
props.currSelect.attributes.attrs.cdata.attrs.condGroups.forEach(each => data.condGroups.push(each))
data.existData = !validateNull(data.condGroups)
},
initCurrVarKeyVal() {
data.cond.valType = props.currSelect.attributes.attrs.cdata.attrs.valType
if (data.condGroups.length <= 0) {
data.cond.varKeyVal = props.currSelect.attributes.attrs.cdata.attrs.varKeyVal
let httpMethod = props.currSelect.attributes.attrs.cdata.attrs.httpMethod
if (httpMethod) data.cond.httpMethod = httpMethod
} else {
data.cond.groupsType = data.condGroups[0].groupsType
}
let httpParams = props.currSelect.attributes.attrs.cdata.attrs.httpParams;
if (!validateNull(httpParams)) {
data.httpUrlParamsVisible = true
}
return data.cond.valType
},
handleLinkFlowNodeIds() {
handleLinkFlowNodeIds(data, props)
},
changeLinkFlowNodeIds(type) {
data.modifyPointType = type
changeLinkFlowNodeIds(data, props, methods, $emit)
},
changeTabPane(val) {
methods.validateCondData()
methods.handleCondValType()
methods.handleLinkFlowNodeIds()
}
}
// 监听双向绑定
watch(
() => props.currSelect,
(val) => {
if (Object.keys(val).length === 0 || !props.attrConfigVisible) {
return
}
methods.changeTabPane(val)
}
);
</script>
<style lang="scss">
@import '../assets/style/flow-attr.scss';
</style>

View File

@@ -0,0 +1,125 @@
<template>
<el-table :data="props.currSelect.attributes.attrs.cdata.defJob.currRunJobs" style="width: 100%">
<el-table-column type="index" :label="t('createTable.index')" width="80">
<template #header>
<el-button icon="Plus" size="small" type="primary" circle @click="onAddItem"></el-button>
</template>
<template #default="scope">
<el-button icon="Minus" size="small" type="danger" circle @click="handleDelete(scope.$index, scope.row)"
:disabled="scope.row.isConfigJob === '1'"></el-button>
</template>
</el-table-column>
<el-table-column prop="sort" :label="t('runJob.sort')" show-overflow-tooltip width="70">
<template #default="scope">
<el-input v-model="scope.row.sort" :placeholder="t('runJob.inputSortTip')"></el-input>
</template>
</el-table-column>
<el-table-column prop="jobName" :label="t('runJob.jobName')" show-overflow-tooltip width="150">
<template #default="scope">
<el-input v-model="scope.row.jobName" :placeholder="t('runJob.inputJobNameTip')"></el-input>
</template>
</el-table-column>
<el-table-column prop="jobType" :label="t('runJob.jobType')" show-overflow-tooltip width="100">
<template #default="scope">
<el-select v-model="scope.row.jobType" :placeholder="t('runJob.inputJobTypeTip')" clearable filterable
@change="handleRoleType(scope.row)">
<el-option v-for="(item, index) in DIC_PROP.JOB_USER_NONE_TYPE" :key="index" :label="item.label" :value="item.value"></el-option>
</el-select>
</template>
</el-table-column>
<el-table-column prop="roleId" :label="t('runJob.roleId')" show-overflow-tooltip width="130">
<template #default="scope">
<el-tooltip content="请输入名称进行模糊搜索" placement="top">
<el-select v-model="scope.row.roleId" :placeholder="t('runJob.inputRoleIdTip')" clearable filterable
remote :remote-method="(query) => remoteMethodAll(query, scope.row.jobType)" :reserve-keyword="false">
<el-option v-for="(item, index) in dicData.users" :key="index" :label="item.name" :value="item.userId"
v-if="scope.row.jobType === DIC_PROP.JOB_USER_TYPE[0].value"></el-option>
<el-option v-for="(item, index) in dicData.roles" :key="index" :label="item.roleName" :value="item.roleId"
v-if="scope.row.jobType === DIC_PROP.JOB_USER_TYPE[1].value"></el-option>
<el-option v-for="(item, index) in dicData.posts" :key="index" :label="item.postName" :value="item.postId"
v-if="scope.row.jobType === DIC_PROP.JOB_USER_TYPE[2].value"></el-option>
<el-option v-for="(item, index) in dicData.depts" :key="index" :label="item.name" :value="item.deptId"
v-if="scope.row.jobType === DIC_PROP.JOB_USER_TYPE[3].value"></el-option>
</el-select>
</el-tooltip>
</template>
</el-table-column>
<el-table-column prop="userId" :label="t('runJob.userId')" show-overflow-tooltip>
<template #default="scope">
<convert-name :options="dicData.userId" :value="scope.row.userId"
:valueKey="'userId'" :showKey="'name'"></convert-name>
</template>
</el-table-column>
<el-table-column prop="startTime" :label="t('runJob.startTime')" show-overflow-tooltip/>
<el-table-column prop="status" :label="t('runJob.status')" show-overflow-tooltip>
<template #default="scope">
<dict-tag :options="DIC_PROP.NODE_STATUS" :value="scope.row.status"></dict-tag>
</template>
</el-table-column>
</el-table>
</template>
<script setup lang="ts" name="FlowCurrJob">
import {useI18n} from "vue-i18n";
import {useMessage} from "/@/hooks/message";
import {onFormLoadedUrl, onLoadDicUrl, remoteMethodAllByKey} from "../../components/convert-name/convert";
import {handleChangeJobType} from "../../index";
import {DIC_PROP} from "../../support/dict-prop";
import {validateNull} from "../../../utils/validate";
import {PROP_CONST} from "../../support/prop-const";
const {proxy} = getCurrentInstance();
const {t} = useI18n();
const $message = useMessage();
const props = defineProps({
currSelect: {
type: Object,
default: null,
},
});
// 定义字典
const dicData = reactive({});
const onLoad = onLoadDicUrl();
const onFormLoaded = onFormLoadedUrl({key: "userId"}, ...PROP_CONST.LOAD_USER_ROLE);
onMounted(async () => {
// await onLoad(dicData);
await onFormLoadedCurrRunJobs()
})
const onAddItem = () => {
let jobName = props.currSelect.attributes.attrs.cdata.defJob.jobName;
let obj = {sort: 1, jobName: jobName, jobType: DIC_PROP.JOB_USER_NONE_TYPE[0].value, roleId: null, userId: null,
startTime: null, status: DIC_PROP.NODE_STATUS[0].value};
props.currSelect.attributes.attrs.cdata.defJob.currRunJobs.push(obj);
}
const handleDelete = (index: number, row: any) => {
let currRunJobs = props.currSelect.attributes.attrs.cdata.defJob.currRunJobs;
if (currRunJobs.length === 1) {
useMessage().warning("当前节点参与者至少需存在一个参与者");
return
}
props.currSelect.attributes.attrs.cdata.defJob.currRunJobs.splice(index, 1)
}
function remoteMethodAll(query: string, jobType) {
remoteMethodAllByKey(onLoad, dicData, query, jobType)
}
function onFormLoadedCurrRunJobs() {
let currRunJobs = props.currSelect.attributes.attrs.cdata.defJob.currRunJobs
if (validateNull(currRunJobs)) return
onFormLoaded(dicData, currRunJobs)
}
function handleRoleType(row) {
handleChangeJobType(dicData, row)
}
</script>
<style lang="scss">
@import '../assets/style/flow-attr.scss';
</style>

View File

@@ -0,0 +1,207 @@
<template>
<el-form label-position="left" class="flow-config-attr" label-width="160px">
<el-form-item label="查询表单Http请求地址">
<el-input class="input-attr" placeholder="可输入全路径或相对路径" v-model="props.flowData.attrs.queryOrder"
clearable>
<template #prepend>
<el-select v-model="props.flowData.attrs.queryMethod">
<el-option v-for="(item, index) in DIC_PROP.HTTP_METHODS" :key="index" :label="item.label" :value="item.value"></el-option>
</el-select>
</template>
</el-input>
<el-tooltip placement="top">
<template #content>查询表单信息接口可输入全路径或相对路径</template>
<el-icon style="margin-left: 10px">
<QuestionFilled/>
</el-icon>
</el-tooltip>
</el-form-item>
<el-form-item label="更新表单Http请求地址">
<el-input class="input-attr" placeholder="可输入全路径或相对路径" v-model="props.flowData.attrs.updateOrder"
clearable>
<template #prepend>
<el-select v-model="props.flowData.attrs.updateMethod">
<el-option v-for="(item, index) in DIC_PROP.HTTP_METHODS" :key="index" :label="item.label" :value="item.value"></el-option>
</el-select>
</template>
</el-input>
<el-tooltip placement="top">
<template #content>更新表单信息接口可输入全路径或相对路径</template>
<el-icon style="margin-left: 10px">
<QuestionFilled/>
</el-icon>
</el-tooltip>
</el-form-item>
<el-divider>关联表单Http请求头</el-divider>
<el-table :data="data.httpParams.filter(f => f.paramFrom === DIC_PROP.PARAM_FROM[0].value)"
border style="width: 100%; margin-bottom: 10px" max-height="500">
<el-table-column type="index" :label="t('jfI18n.operate')" width="80">
<template #header>
<el-button icon="Plus" size="small" type="primary" circle
@click="methods.onAddItem(DIC_PROP.PARAM_FROM[0].value, true)"></el-button>
</template>
<template #default="scope">
<el-button icon="Minus" size="small" type="danger" circle
@click="methods.handleHttpUrlDelete(scope.$index, scope.row)"></el-button>
</template>
</el-table-column>
<el-table-column prop="targetProp" :label="t('jfAttr.requestHeader')" show-overflow-tooltip>
<template #default="scope">
<el-input v-model="scope.row.targetProp" :placeholder="scope.row.paramValType === DIC_PROP.PARAM_VAL_TYPE[0].value ? scope.row.varKeyVal : null"
clearable></el-input>
</template>
</el-table-column>
<el-table-column prop="paramValType" :label="t('jfAttr.paramValType')" width="145px">
<template #default="scope">
<el-select v-model="scope.row.paramValType" clearable>
<el-option v-for="(item, index) in DIC_PROP.SYS_PARAM_VAL_TYPE" :key="index" :label="item.label" :value="item.value"></el-option>
</el-select>
</template>
</el-table-column>
<el-table-column prop="varKeyVal" :label="t('jfAttr.sysVarKeyVal')" show-overflow-tooltip>
<template #default="scope">
<el-select v-model="scope.row.varKeyVal"
v-if="scope.row.paramValType === DIC_PROP.PARAM_VAL_TYPE[0].value"
clearable filterable>
<el-option-group
v-for="(group, index) in data.allFieldPerms"
:key="index"
:label="group.label">
<el-option
v-for="(item, index) in group.options"
:disabled="item.prop.indexOf('_define_') !== -1"
:key="index"
:label="item.label"
:value="item.prop">
</el-option>
</el-option-group>
</el-select>
<el-input v-else v-model="scope.row.varKeyVal" clearable> </el-input>
</template>
</el-table-column>
</el-table>
</el-form>
</template>
<script setup lang="ts" name="FlowHttpParam">
import {useI18n} from "vue-i18n";
import {useMessage} from "/@/hooks/message";
import {validateNull} from "/@/utils/validate";
import {handleFieldProp} from "../../utils/form-perm";
import {DIC_PROP} from "../../support/dict-prop";
import {PROP_CONST} from "../../support/prop-const";
import {deepClone} from "/@/utils/other";
const {proxy} = getCurrentInstance();
const {t} = useI18n();
const $message = useMessage();
const props = defineProps({
currFlowForm: {
type: Object,
default: null,
},
flowData: {
type: Object,
default: null,
},
attrConfigVisible: null
});
const data = reactive({
allFieldPerms: [],
formFieldPerms: [],
httpParams: [],
paramType: '0',
})
// 定义字典
onMounted(async () => {
if (!props.attrConfigVisible) return
methods.changeTabPane()
})
const methods = {
listFormFieldPerms() {
if (validateNull(data.formFieldPerms)) {
let formFieldPerms = deepClone(PROP_CONST.SYS_FIELDS);
handleFieldProp(formFieldPerms, null)
data.allFieldPerms = [{label: '系统字段', options: formFieldPerms}]
}
},
onAddItem(paramFrom, isAdd) {
methods.listFormFieldPerms()
let value = DIC_PROP.PARAM_VAL_TYPE[0].value;
if (data.httpParams.length > 0) {
let find = data.httpParams.filter(f => f.paramFrom === paramFrom).find(f => !f.varKeyVal || (f.paramValType !== value && !f.targetProp));
if (find) {
if (isAdd) {
if (!find.varKeyVal) {
$message.warning("请先填写 表单字段")
return
}
if (find.paramValType !== value && !find.targetProp) {
$message.warning("请先填写 请求头")
return
}
}
}
if (!isAdd) data.httpParams.splice(0, data.httpParams.length);
}
let obj = {paramFrom: paramFrom, varKeyVal: null, paramValType: value, targetProp: null};
data.httpParams.push(obj);
methods.changeHttpUrlParams()
},
handleHttpUrlDelete(index: number, row: any) {
let splice = data.httpParams.filter(f => f.paramFrom === row.paramFrom);
splice.splice(index, 1);
data.httpParams = splice
methods.changeHttpUrlParams()
},
validateHttpUrlData() {
// 兼容老版本
let httpParams = props.flowData.attrs.orderParams;
if (!httpParams) {
props.flowData.attrs.orderParams = []
}
let queryMethod = props.flowData.attrs.queryMethod;
if (!queryMethod) props.flowData.attrs.queryMethod = 'GET'
let updateMethod = props.flowData.attrs.updateMethod;
if (!updateMethod) props.flowData.attrs.updateMethod = 'PUT'
data.httpParams = props.flowData.attrs.orderParams
},
changeHttpUrlParams() {
props.flowData.attrs.orderParams = data.httpParams
},
changeTabPane() {
methods.validateHttpUrlData()
methods.listFormFieldPerms()
}
}
// 监听双向绑定
watch(
() => props.attrConfigVisible,
(val) => {
if (!val) return
methods.changeTabPane()
}
);
</script>
<style lang="scss">
@import '../assets/style/flow-attr.scss';
</style>

View File

@@ -0,0 +1,295 @@
<template>
<el-form v-if="methods.validateNode(props.currSelect)"
label-position="left" class="flow-config-attr" label-width="130px">
<el-form-item label="全部只读或可编辑" prop="formPermType">
<el-tooltip placement="top">
<template #content>1对当前节点表单的字段全部只读 全部可编辑默认全部可编辑
<br/> 2可在下方进一步约束权限如这里选择全部只读下方配置某些字段可编辑
</template>
<el-radio-group v-model="props.currSelect.attributes.attrs.cdata.attrs.formPermType">
<el-radio v-for="(item, index) in DIC_PROP.ALL_FORM_PERM_TYPE" :key="index" :label="item.value" style="width: 135px">
{{ item.label }}
</el-radio>
</el-radio-group>
</el-tooltip>
<el-tooltip placement="top" v-if="props.currSelect.attributes.attrs.cdata.attrs.formPermType">
<template #content>清空后可在下方单个字段配置权限或业务侧自行判断</template>
<el-button text type="primary" icon="delete" @click="props.currSelect.attributes.attrs.cdata.attrs.formPermType = null">
清空
</el-button>
</el-tooltip>
</el-form-item>
<el-divider>请选择表单名称</el-divider>
<el-form-item label="PC待办页面">
<el-tooltip placement="top">
<template #content>若下拉选项为空或没有期望的表单请先在节点属性-PC待办页面选择</template>
<el-select class="input-attr"
v-model="props.currSelect.attributes.attrs.cdata.attrs.formId"
@change="methods.changeNodeFormId"
clearable filterable placeholder="请选择节点属性-PC待办页面">
<template v-for="(item, index) in props.formIds">
<el-option
v-if="methods.filterNodeFormId(item)"
:key="index"
:label="item.formName+' V'+item.version"
:value="item.id">
</el-option>
</template>
</el-select>
</el-tooltip>
<el-button @click="methods.addFormPermission" type="primary" round v-if="props.isShowAdd"
style="margin-left: 10px;">新增字段
</el-button>
<el-button @click="methods.resetFormPerm" type="primary" size="small" round v-if="!props.isShowAdd"
style="margin-left: 10px;">重置权限
</el-button>
</el-form-item>
<el-divider>参与者可以操作表单字段权限</el-divider>
<el-empty description="表单字段权限列表为空" style="margin: 10px 230px" v-if="validateNull(data.formFieldPerms)">
</el-empty>
<el-form-item v-for="(item, index) in data.formFieldPerms" v-else
:label="item.label"
:key="index">
<el-input style="width: 131px; margin-right: 10px" v-model="item.label" v-if="props.isShowAdd"></el-input>
<el-input style="width: 131px; margin-right: 10px" v-model="item.prop" v-if="props.isShowAdd"></el-input>
<el-radio-group v-model="item.permType">
<el-radio v-for="(item, index) in DIC_PROP.FORM_PERM_TYPE" :key="index"
:disabled="props.currSelect.attributes.attrs.cdata.attrs.formPermType === item.value"
:label="item.value" style="width: 86px">
{{ item.label }}
</el-radio>
</el-radio-group>
<el-button @click.prevent="methods.removeFormPermission(item.prop)" type="primary" size="small" round>
删除
</el-button>
</el-form-item>
</el-form>
</template>
<script setup lang="ts" name="FlowFormPerm">
import {useI18n} from "vue-i18n";
import {useMessage} from "/@/hooks/message";
import {validateNull} from "/@/utils/validate";
import {parseWithFunctions, validateRunFlow} from "../../index";
import {buildFieldPerms, handleFormFieldPerms} from "../../utils/form-perm";
import {CommonNodeType, HighNodeType} from "../config/type";
import {listFormOption} from "/@/api/jsonflow/form-option";
import {DIC_PROP} from "../../support/dict-prop";
import {deepClone} from "/@/utils/other";
const { proxy } = getCurrentInstance();
const {t} = useI18n();
const $message = useMessage();
const props = defineProps({
currFlowForm: {
type: Object,
default: null,
},
currSelect: {
type: Object,
default: null,
},
flowData: {
type: Object,
default: null,
},
formIds: {
type: Array,
default: null,
},
isShowAdd: {
type: Boolean,
default: false,
},
});
const data = reactive({
formFieldPerms: [],
})
onMounted(() => {
methods.changeTabPane(props.currSelect)
})
const methods = {
validateCurrSelectAttrs(currSelect?) {
if (!currSelect) currSelect = props.currSelect;
if (validateNull(currSelect.attributes)) return false
return true
},
validateNode(currSelect) {
if (!methods.validateCurrSelectAttrs()) return false;
let type = currSelect.attributes.attrs.cdata.type;
return type === CommonNodeType.START || type === CommonNodeType.END || type === CommonNodeType.SERIAL || type === CommonNodeType.PARALLEL || type === HighNodeType.VIRTUAL
},
filterNodeFormId(item) {
let pcTodoUrl = props.currSelect.attributes.attrs.cdata.attrs.pcTodoUrl
if (!validateNull(pcTodoUrl)) {
return pcTodoUrl.includes(item.id)
} else return false
},
changeNodeFormId(value) {
if (!methods.validateNode(props.currSelect)) return;
if (!value) {
methods.clearPermData()
return
}
data.formFieldPerms = []
let find = props.formIds.find(f => f.id === value);
if (validateNull(find.formInfo)) {
methods.handleFormFieldPerms(find)
} else {
let formInfo = parseWithFunctions(find.formInfo, true)
buildFieldPerms(data.formFieldPerms, formInfo.widgetList);
}
methods.setNodeAttrsFormFieldPerms()
},
handleFormFieldPerms(find) {
// 无需前缀
let isReturn = handleFormFieldPerms(data, $message, find, '');
if (isReturn) return
if (find.isCurrForm === '1' && !validateNull(find.formFieldPerms)) {
methods.buildCustomFormPerm(find.formFieldPerms)
return;
}
// 已配置或全部字段
methods.handleCustomFormPerm(find.id, null, props.flowData.attrs.id , props.currSelect.id)
},
initNodeFormId() {
if (Object.keys(props.currSelect).length === 0) return
if (!methods.validateNode(props.currSelect)) return;
let formId = props.currSelect.attributes.attrs.cdata.attrs.formId;
if (!formId) {
methods.clearPermData()
return
}
// formId必存在权限
data.formFieldPerms = props.currSelect.attributes.attrs.cdata.attrs.formFieldPerms;
methods.initAddFormPermission(formId)
},
async initAddFormPermission(formId) {
let find = props.formIds.find(f => f.id === formId);
if (!find) return
let formFieldPerms = []
if (find.type === DIC_PROP.FORM_TYPE[1].value) {
if (find.isCurrForm === '1') {
formFieldPerms = find.formFieldPerms
} else {
formFieldPerms = await methods.reqCustomFormPerm(formId, DIC_PROP.FORM_DATA_TYPE[0].value, null, null)
}
}
if (find.type !== DIC_PROP.FORM_TYPE[1].value) {
if (!validateNull(find.formInfo)) {
let formInfo = parseWithFunctions(find.formInfo, true)
buildFieldPerms(formFieldPerms, formInfo.widgetList);
}
}
if (validateNull(formFieldPerms)) return
formFieldPerms.forEach(each => {
let index = data.formFieldPerms.findIndex(f => f.propId ? each.propId === f.propId : each.prop === f.prop);
if (index !== -1) {
let exist = data.formFieldPerms[index]
data.formFieldPerms.splice(index, 1);
each.permType = exist.permType
data.formFieldPerms.splice(index, 0, each);
return
}
data.formFieldPerms.push(each)
})
},
removeFormPermission(prop) {
if (data.formFieldPerms.length === 0) return
data.formFieldPerms = data.formFieldPerms.filter(f => f.prop !== prop)
methods.setNodeAttrsFormFieldPerms()
},
setNodeAttrsFormFieldPerms() {
// 相同引用
props.currSelect.attributes.attrs.cdata.attrs.formFieldPerms = data.formFieldPerms
},
addFormPermission() {
let formId = props.currSelect.attributes.attrs.cdata.attrs.formId;
if (!formId) {
$message.warning("请先选择表单名称")
return
}
if (!data.formFieldPerms) data.formFieldPerms = []
let number = data.formFieldPerms.length + 1;
data.formFieldPerms.unshift({
prop: 'propName' + number,
label: '请输入字段名称'
})
methods.setNodeAttrsFormFieldPerms()
},
async reqCustomFormPerm(formId, type, defFlowId, flowNodeId) {
if (!type) type = DIC_PROP.FORM_DATA_TYPE[1].value
// 判断流程实例独立配置
let flowInstId = validateRunFlow(props);
let resp = await listFormOption({
type: type, formType: DIC_PROP.FORM_TYPE[1].value, formId: formId,
flowInstId: flowInstId, defFlowId: defFlowId, flowNodeId: flowNodeId
}).catch(() => {
$message.error("获取系统表单字段权限失败");
})
return resp.data
},
async handleCustomFormPerm(formId, type, defFlowId, flowNodeId) {
let formFieldPerms = await methods.reqCustomFormPerm(formId, type, defFlowId, flowNodeId)
methods.buildCustomFormPerm(formFieldPerms)
},
buildCustomFormPerm(formFieldPerms) {
if (!validateNull(formFieldPerms)) {
// 不影响表单设计信息
data.formFieldPerms = deepClone(formFieldPerms)
methods.setNodeAttrsFormFieldPerms()
} else {
methods.clearPermData()
$message.warning("当前选择的系统表单无字段信息,请先在表单设计中录入")
}
},
resetFormPerm(){
let formId = props.currSelect.attributes.attrs.cdata.attrs.formId;
if (!formId) {
$message.warning("请先选择表单名称")
return
}
methods.clearPermData()
let find = props.formIds.find(f => f.id === formId);
if (find.type === DIC_PROP.FORM_TYPE[1].value) {
if (find.isCurrForm === '1' && !validateNull(find.formFieldPerms)) {
methods.buildCustomFormPerm(find.formFieldPerms)
} else {
methods.handleCustomFormPerm(formId, DIC_PROP.FORM_DATA_TYPE[0].value, null, null)
}
} else {
methods.changeNodeFormId(formId)
}
},
clearPermData() {
data.formFieldPerms = []
delete props.currSelect.attributes.attrs.cdata.attrs.formFieldPerms
},
changeTabPane(val) {
methods.initNodeFormId()
}
}
// 监听双向绑定
watch(
() => props.currSelect,
(val) => {
if (Object.keys(val).length === 0) {
return
}
methods.changeTabPane(val)
}
);
</script>
<style lang="scss">
@import '../assets/style/flow-attr.scss';
</style>

View File

@@ -0,0 +1,352 @@
<template>
<el-divider>请求头</el-divider>
<el-table :data="data.httpParams.filter(f => f.paramFrom === DIC_PROP.PARAM_FROM[0].value)"
border style="width: 100%; margin-bottom: 10px" max-height="500">
<el-table-column type="index" :label="t('jfI18n.operate')" width="80">
<template #header>
<el-button icon="Plus" size="small" type="primary" circle
@click="methods.onAddItem(DIC_PROP.PARAM_FROM[0].value, true)"></el-button>
</template>
<template #default="scope">
<el-button icon="Minus" size="small" type="danger" circle
@click="methods.handleHttpUrlDelete(scope.$index, scope.row)"></el-button>
</template>
</el-table-column>
<el-table-column prop="targetProp" :label="t('jfAttr.requestHeader')" show-overflow-tooltip>
<template #default="scope">
<el-input v-model="scope.row.targetProp" :placeholder="scope.row.paramValType === DIC_PROP.PARAM_VAL_TYPE[0].value ? scope.row.varKeyVal : null"
clearable></el-input>
</template>
</el-table-column>
<el-table-column prop="paramValType" :label="t('jfAttr.paramValType')" width="145px">
<template #default="scope">
<el-select v-model="scope.row.paramValType" clearable>
<el-option v-for="(item, index) in DIC_PROP.PARAM_VAL_TYPE" :key="index" :label="item.label" :value="item.value"></el-option>
</el-select>
</template>
</el-table-column>
<el-table-column prop="varKeyVal" :label="t('jfAttr.varKeyVal')" show-overflow-tooltip>
<template #default="scope">
<el-select v-model="scope.row.varKeyVal"
v-if="scope.row.paramValType === DIC_PROP.PARAM_VAL_TYPE[0].value"
clearable filterable>
<el-option-group
v-for="(group, index) in data.allFieldPerms"
:key="index"
:label="group.label">
<el-option
v-for="(item, index) in group.options"
:disabled="item.prop.indexOf('_define_') !== -1"
:key="index"
:label="item.label"
:value="item.prop">
</el-option>
</el-option-group>
</el-select>
<el-input v-else v-model="scope.row.varKeyVal" clearable> </el-input>
</template>
</el-table-column>
</el-table>
<el-divider>请求参数</el-divider>
<el-form-item label="参数类型 :">
<el-radio-group v-model="data.paramType">
<el-radio v-for="(item, index) in DIC_PROP.PARAM_TYPES" :key="index" :label="item.value"
style="width: 56px" @change="methods.handleParamType">
{{ item.label }}
</el-radio>
</el-radio-group>
<el-tooltip placement="top">
<template #content> 表示以指定的参数类型传递请求参数 </template>
<el-icon style="margin-left: 20px">
<QuestionFilled/>
</el-icon>
</el-tooltip>
</el-form-item>
<el-table :data="data.httpParams.filter(f => f.paramFrom === DIC_PROP.PARAM_FROM[1].value)"
border style="width: 100%; margin-bottom: 10px" max-height="500">
<el-table-column type="index" :label="t('jfI18n.operate')" width="80">
<template #header>
<el-button icon="Plus" size="small" type="primary" circle
@click="methods.onAddItem(DIC_PROP.PARAM_FROM[1].value, true)"></el-button>
</template>
<template #default="scope">
<el-button icon="Minus" size="small" type="danger" circle
@click="methods.handleHttpUrlDelete(scope.$index, scope.row)"></el-button>
</template>
</el-table-column>
<el-table-column prop="targetProp" :label="t('jfAttr.requestParam')" show-overflow-tooltip>
<template #default="scope">
<el-input v-model="scope.row.targetProp" :placeholder="scope.row.paramValType === DIC_PROP.PARAM_VAL_TYPE[0].value ? scope.row.varKeyVal : null"
clearable></el-input>
</template>
</el-table-column>
<el-table-column prop="paramValType" :label="t('jfAttr.paramValType')" width="145px">
<template #default="scope">
<el-select v-model="scope.row.paramValType" clearable>
<el-option v-for="(item, index) in DIC_PROP.PARAM_VAL_TYPE" :key="index" :label="item.label" :value="item.value"></el-option>
</el-select>
</template>
</el-table-column>
<el-table-column prop="varKeyVal" :label="t('jfAttr.varKeyVal')" show-overflow-tooltip>
<template #default="scope">
<el-select v-model="scope.row.varKeyVal"
v-if="scope.row.paramValType === DIC_PROP.PARAM_VAL_TYPE[0].value"
clearable filterable>
<el-option-group
v-for="(group, index) in data.allFieldPerms"
:key="index"
:label="group.label">
<el-option
v-for="(item, index) in group.options"
:disabled="item.prop.indexOf('_define_') !== -1"
:key="index"
:label="item.label"
:value="item.prop">
</el-option>
</el-option-group>
</el-select>
<el-input v-else v-model="scope.row.varKeyVal" clearable> </el-input>
</template>
</el-table-column>
</el-table>
<el-divider>返回值</el-divider>
<div v-if="props.httpParamType === DIC_PROP.PARAM_RULE_TYPE[0].value" style="margin: 10px 13px">
<span style="color: #409EFF;font-size: 14px">: 当前Http请求的返回值可以为对象 数组{{ PROP_CONST.TEXT_DESC.condMethodExplain5 }}</span>
</div>
<div v-if="props.httpParamType === DIC_PROP.PARAM_RULE_TYPE[8].value" style="margin: 10px 13px">
<span style="color: #409EFF;font-size: 14px">: 当前Http请求的返回值可以为对象 数组{{ PROP_CONST.TEXT_DESC.condMethodExplain6 }}</span>
</div>
<div v-if="props.httpParamType === DIC_PROP.PARAM_RULE_TYPE[1].value" style="margin: 10px 13px">
<span style="color: #409EFF;font-size: 14px">: 当前Http请求的返回值为字符串 1 ( 满足 ) 0 ( 不满足 )</span>
</div>
<el-table :data="data.httpParams.filter(f => f.paramFrom === DIC_PROP.PARAM_FROM[2].value)"
v-if="props.httpParamType !== DIC_PROP.PARAM_RULE_TYPE[0].value && props.httpParamType !== DIC_PROP.PARAM_RULE_TYPE[1].value
&& props.httpParamType !== DIC_PROP.PARAM_RULE_TYPE[8].value"
border style="width: 100%; margin-bottom: 10px" max-height="500">
<el-table-column type="index" :label="t('jfI18n.operate')" width="80">
<template #header>
<el-button icon="Plus" size="small" type="primary" circle
@click="methods.onAddItem(DIC_PROP.PARAM_FROM[2].value, true)"></el-button>
</template>
<template #default="scope">
<el-button icon="Minus" size="small" type="danger" circle
@click="methods.handleHttpUrlDelete(scope.$index, scope.row)"></el-button>
</template>
</el-table-column>
<el-table-column prop="targetProp" :label="t('jfAttr.returnParam')" show-overflow-tooltip>
<template #default="scope">
<el-input v-model="scope.row.targetProp"
:placeholder="scope.row.paramValType === DIC_PROP.PARAM_VAL_TYPE[0].value ? scope.row.varKeyVal : null"
clearable></el-input>
</template>
</el-table-column>
<el-table-column prop="paramValType" :label="t('jfAttr.paramValType')" width="145px">
<template #default="scope">
<el-switch
v-model="scope.row.paramValType"
:active-value="DIC_PROP.PARAM_VAL_TYPE[0].value"
:active-text="DIC_PROP.PARAM_VAL_TYPE[0].label"
:inactive-value="DIC_PROP.PARAM_VAL_TYPE[2].value"
:inactive-text="DIC_PROP.PARAM_VAL_TYPE[2].label"
inline-prompt>
</el-switch>
</template>
</el-table-column>
<el-table-column prop="varKeyVal" :label="t('flowClazz.varKeyVal')" show-overflow-tooltip>
<template #default="scope">
<el-select v-model="scope.row.varKeyVal"
clearable filterable>
<el-option-group
v-for="(group, index) in data.allFieldPerms"
:key="index"
:label="group.label">
<el-option
v-for="(item, index) in group.options"
:disabled="item.prop.indexOf('_define_') !== -1"
:key="index"
:label="item.label"
:value="item.prop">
</el-option>
</el-option-group>
</el-select>
</template>
</el-table-column>
</el-table>
</template>
<script setup lang="ts" name="FlowHttpParam">
import {useI18n} from "vue-i18n";
import {useMessage} from "/@/hooks/message";
import {validateNull} from "/@/utils/validate";
import {buildSysFieldsFormOption} from "../../utils/form-perm";
import {DIC_PROP} from "../../support/dict-prop";
const {proxy} = getCurrentInstance();
const {t} = useI18n();
const $message = useMessage();
const props = defineProps({
currFlowForm: {
type: Object,
default: null,
},
httpParam: {
type: Object,
default: null,
},
httpParamType: {
type: String,
default: null,
},
flowData: {
type: Object,
default: null,
},
});
const data = reactive({
allFieldPerms: [],
formFieldPerms: [],
httpParams: [],
paramType: '0',
})
// 定义字典
onMounted(async () => {
methods.changeTabPane(props.httpParam)
})
const methods = {
initParamType() {
let find = data.httpParams.find(f => f.paramFrom === DIC_PROP.PARAM_FROM[1].value);
if (find) data.paramType = find.paramType
},
handleParamType(val) {
let splice = data.httpParams.filter(f => f.paramFrom === DIC_PROP.PARAM_FROM[1].value);
splice.forEach(each => each.paramType = val)
let res = data.httpParams.filter(f => f.paramFrom === DIC_PROP.PARAM_FROM[0].value);
let res2 = data.httpParams.filter(f => f.paramFrom === DIC_PROP.PARAM_FROM[2].value);
splice.push(...res)
splice.push(...res2)
data.httpParams = splice
methods.changeHttpUrlParams()
},
listFormFieldPerms() {
if (validateNull(data.formFieldPerms)) {
buildSysFieldsFormOption(data, props, $message)
}
},
onAddItem(paramFrom, isAdd) {
methods.listFormFieldPerms()
let value = DIC_PROP.PARAM_VAL_TYPE[0].value;
if (data.httpParams.length > 0) {
let find = data.httpParams.filter(f => f.paramFrom === paramFrom).find(f => !f.varKeyVal || (f.paramValType !== value && !f.targetProp));
if (find) {
if (isAdd) {
if (!find.varKeyVal) {
$message.warning("请先填写 表单字段")
return
}
if (find.paramValType !== value && !find.targetProp) {
if (DIC_PROP.PARAM_FROM[0].value === paramFrom) {
$message.warning("请先填写 请求头")
} else if (DIC_PROP.PARAM_FROM[1].value === paramFrom) {
$message.warning("请先填写 请求参数")
} else {
$message.warning("请先填写 返回值")
}
return
}
}
}
if (!isAdd) data.httpParams.splice(0, data.httpParams.length);
}
let obj = {paramFrom: paramFrom, varKeyVal: null, paramValType: value, targetProp: null};
if (paramFrom === DIC_PROP.PARAM_FROM[1].value) {
obj.paramType = data.paramType
}
data.httpParams.push(obj);
methods.changeHttpUrlParams()
},
handleHttpUrlDelete(index: number, row: any) {
let splice = data.httpParams.filter(f => f.paramFrom === row.paramFrom);
splice.splice(index, 1);
if (DIC_PROP.PARAM_FROM[0].value === row.paramFrom) {
let res = data.httpParams.filter(f => f.paramFrom === DIC_PROP.PARAM_FROM[1].value);
let res2 = data.httpParams.filter(f => f.paramFrom === DIC_PROP.PARAM_FROM[2].value);
splice.push(...res)
splice.push(...res2)
} else if (DIC_PROP.PARAM_FROM[1].value === row.paramFrom) {
let res = data.httpParams.filter(f => f.paramFrom === DIC_PROP.PARAM_FROM[0].value);
let res2 = data.httpParams.filter(f => f.paramFrom === DIC_PROP.PARAM_FROM[2].value);
splice.push(...res)
splice.push(...res2)
} else if (DIC_PROP.PARAM_FROM[2].value === row.paramFrom) {
let res = data.httpParams.filter(f => f.paramFrom === DIC_PROP.PARAM_FROM[0].value);
let res2 = data.httpParams.filter(f => f.paramFrom === DIC_PROP.PARAM_FROM[1].value);
splice.push(...res)
splice.push(...res2)
}
data.httpParams = splice
methods.changeHttpUrlParams()
},
validateHttpUrlData() {
// 兼容老版本
let httpParams = props.httpParam.httpParams;
if (!httpParams) {
props.httpParam.httpParams = []
}
data.httpParams = props.httpParam.httpParams
},
changeHttpUrlParams() {
props.httpParam.httpParams = data.httpParams
},
changeTabPane(val) {
methods.validateHttpUrlData()
methods.listFormFieldPerms()
methods.initParamType()
}
}
// 监听双向绑定
watch(
() => props.httpParam,
(val) => {
if (Object.keys(val).length === 0) {
return
}
methods.changeTabPane(val)
}
);
</script>
<style lang="scss">
@import '../assets/style/flow-attr.scss';
</style>

View File

@@ -0,0 +1,360 @@
<template>
<div>
<el-tabs class="flow-attr"
v-model="data.activeKey">
<el-tab-pane name="flow-method">
<template #label>
<div>
<el-icon style="vertical-align: middle;margin-right: 3px">
<User/>
</el-icon>
<span style="vertical-align: middle;">选择审批对象</span>
</div>
</template>
<el-form label-position="left" class="flow-config-attr" label-width="170px">
<div style="margin: 5px 20px">
<span style="color: red;font-size: 14px">: 当前审批对象可自定义任意扩展(只需写好接口即可), 满足您分配参与者复杂的场景</span>
</div>
<el-divider> 审批对象设置 </el-divider>
<el-form-item label="审批对象类型">
<el-radio-group style="width: 283px" @change="methods.handleUserKeyValFrom"
v-model="data.userKeyValFrom">
<el-radio v-for="(item, index) in DIC_PROP.FLOW_METHOD_TYPE" :key="index" :label="item.value"
:disabled="item.value === '-1'">
{{ item.label }}
</el-radio>
</el-radio-group>
</el-form-item>
<template v-if="data.userKeyValFrom === '0'">
<div style="margin-left: 15px"> 发起人本人将作为审批人</div>
</template>
<template v-if="data.userKeyValFrom === '1'">
<el-divider> </el-divider>
<el-form-item label="谁的主管">
<el-tooltip content="请输入用户名称进行模糊搜索" placement="top">
<el-select class="input-attr"
v-model="data.whoseLeader" @change="methods.changeWhoseLeader"
clearable filterable
remote :remote-method="remoteMethod" :reserve-keyword="false">
<template v-for="(item, index) in dicData.users" :key="index">
<el-option :label="item.name"
:value="item.userId">
</el-option>
</template>
</el-select>
</el-tooltip>
<el-tooltip placement="top" content="用于指定哪个用户的部门主管">
<el-icon style="margin-left: 10px"><QuestionFilled /></el-icon>
</el-tooltip>
</el-form-item>
<el-divider> </el-divider>
<el-form-item label="指定主管">
<div>
<span>&lt;{{ data.whoseLeaderName }} 用户&gt;的第 &nbsp</span>
<el-input-number :min="1" :max="20" :step="1" style="width: 150px"
v-model="data.leaderLevel"></el-input-number>
<span> &nbsp级主管</span>
<div style="color: #409EFF; font-size: small;">👉 指定主管为 {{ data.leaderLevel }} 级主管</div>
</div>
</el-form-item>
<el-divider> </el-divider>
<el-form-item label="提取规则">
<el-radio-group style="width: 283px"
v-model="data.levelExtract">
<el-radio v-for="(item, index) in EXTRACTS.SINGLE_EXTRACT" :key="index" :label="item.value">
{{ item.label }}
</el-radio>
</el-radio-group>
</el-form-item>
</template>
<template v-if="data.userKeyValFrom === '2'">
<el-divider> </el-divider>
<el-form-item label="谁的主管">
<el-tooltip content="请输入用户名称进行模糊搜索" placement="top">
<el-select class="input-attr"
v-model="data.whoseLeader" @change="methods.changeWhoseLeader"
clearable filterable
remote :remote-method="remoteMethod" :reserve-keyword="false">
<template v-for="(item, index) in dicData.users" :key="index">
<el-option :label="item.name"
:value="item.userId">
</el-option>
</template>
</el-select>
</el-tooltip>
<el-tooltip placement="top" content="用于指定哪个用户的部门主管">
<el-icon style="margin-left: 10px"><QuestionFilled /></el-icon>
</el-tooltip>
</el-form-item>
<el-divider> </el-divider>
<el-form-item label="审批终点">
<el-radio-group style="width: 200px" v-model="data.auditEndpoint">
<el-radio label="0">直到最上层主管</el-radio>
<el-radio label="1">不超过&lt;{{ data.whoseLeaderName }} 用户&gt;</el-radio>
</el-radio-group>
<div class="audit-endpoint">
&nbsp<el-input-number :min="1" :max="20" :step="1" style="width: 80px" controls-position="right"
v-model="data.leaderLevel"></el-input-number>&nbsp级主管
</div>
</el-form-item>
<el-divider> </el-divider>
<el-form-item label="依次审批顺序">
<el-tooltip content="当节点属性【多人审批方式】为依次审批时,审批顺序由选择的顺序决定" placement="bottom">
<el-radio-group style="width: 200px" v-model="data.seqAuditSort">
<el-radio label="0">下级到上级</el-radio>
<el-radio label="1">上级到下级</el-radio>
</el-radio-group>
</el-tooltip>
<el-button v-if="data.seqAuditSort" text type="primary" icon="delete" @click="data.seqAuditSort = null">
清空
</el-button>
</el-form-item>
<el-divider> </el-divider>
<el-form-item label="提取规则">
<el-radio-group style="width: 283px"
v-model="data.levelExtract">
<el-radio v-for="(item, index) in EXTRACTS.MULTI_EXTRACT" :key="index" :label="item.value">
{{ item.label }}
</el-radio>
</el-radio-group>
<el-tooltip placement="top" content="必须保证最终至少存在一个主管">
<el-icon class="audit-endpoint-extract"><QuestionFilled /></el-icon>
</el-tooltip>
</el-form-item>
</template>
<template v-if="data.userKeyValFrom === '3'">
<el-divider> </el-divider>
<el-form-item label="哪个部门">
<el-tooltip content="请输入部门名称进行模糊搜索" placement="top">
<el-select class="input-attr"
v-model="data.appointDeptId"
clearable filterable
remote :remote-method="remoteMethodDept" :reserve-keyword="false">
<template v-for="(item, index) in dicData.depts" :key="index">
<el-option :label="item.name"
:value="item.deptId">
</el-option>
</template>
</el-select>
</el-tooltip>
<el-tooltip placement="top" content="用于指定哪个部门的主管">
<el-icon style="margin-left: 10px"><QuestionFilled /></el-icon>
</el-tooltip>
</el-form-item>
<el-divider> </el-divider>
<el-form-item label="提取规则">
<el-radio-group style="width: 283px"
v-model="data.levelExtract">
<el-radio v-for="(item, index) in EXTRACTS.DEPT_EXTRACT" :key="index" :label="item.value">
{{ item.label }}
</el-radio>
</el-radio-group>
</el-form-item>
</template>
<template v-if="data.userKeyValFrom === '4'">
<el-divider> </el-divider>
<el-form-item label="表单字段">
<el-select v-model="data.userKeyVal"
clearable
filterable>
<el-option
v-for="(item, index) in data.formFieldPerms"
:key="index"
:label="item.label"
:value="item.prop">
</el-option>
</el-select>
</el-form-item>
</template>
<el-button
type="primary" round style="margin-left: 185px; margin-top: 50px; width: 200px"
@click="methods.confirmMethods">
确定
</el-button>
</el-form>
</el-tab-pane>
<el-tab-pane name="flow-rule">
<template #label>
<div>
<el-icon style="vertical-align: middle;margin-right: 3px">
<Setting/>
</el-icon>
<span style="vertical-align: middle;">设置审批规则</span>
</div>
</template>
<el-form label-position="left" class="flow-config-attr" label-width="150px">
<flow-user-rule :currSelect="props.currSelect" :currFlowForm="props.currFlowForm" :flowData="props.flowData"></flow-user-rule>
</el-form>
</el-tab-pane>
<el-tab-pane name="flow-curr-job" v-if="methods.validateNodeJobCurrJob()">
<template #label>
<div>
<el-icon style="vertical-align: middle;margin-right: 3px">
<Setting/>
</el-icon>
<span style="vertical-align: middle;">节点参与者列表</span>
</div>
</template>
<el-form label-position="left" class="flow-config-attr" label-width="150px">
<flow-curr-job :currSelect="props.currSelect"></flow-curr-job>
</el-form>
</el-tab-pane>
</el-tabs>
</div>
</template>
<script setup lang="ts" name="FlowMethod">
import {useI18n} from "vue-i18n";
import {useMessage} from "/@/hooks/message";
import {validateNull} from "/@/utils/validate";
import {onFormLoadedUrl, onLoadDicUrl, remoteMethodByKey} from "/@/flow/components/convert-name/convert";
import {validateListFormOption} from "../../utils/form-perm";
import {parseUserKeyValName, revParseUserKeyValName, revParseWhoseLeaderName} from "./index";
const { proxy } = getCurrentInstance();
const FlowUserRule = defineAsyncComponent(() => import('./flow-user-rule.vue'));
const FlowCurrJob = defineAsyncComponent(() => import('./flow-curr-job.vue'));
const $emit = defineEmits(["openFlowMethods"]);
const {t} = useI18n();
const $message = useMessage();
const props = defineProps({
currFlowForm: {
type: Object,
default: null,
},
currSelect: {
type: Object,
default: {},
},
flowData: {
type: Object,
default: null,
},
});
const EXTRACTS = {
SINGLE_EXTRACT: [{
label: '无主管时由上级主管代审批',
value: '0'
}, {
label: '无主管时[报错]处理',
value: '1'
}],
MULTI_EXTRACT: [{
label: '无主管时由上级主管代审批,直到满足等级的人数',
value: '0'
}, {
label: '无主管时[按空]处理',
value: '1'
}],
DEPT_EXTRACT: [{
label: '无主管时由上级主管代审批',
value: '0'
}, {
label: '无主管时[报错]处理',
value: '1'
}],
}
const data = reactive({
activeKey: 'flow-method',
formFieldPerms: [],
userKeyValFrom: '1',
whoseLeader: null,
whoseLeaderName: '请先指定',
leaderLevel: 1,
auditEndpoint: '0',
seqAuditSort: null,
levelExtract: '0',
appointDeptId: null,
userKeyVal: null,
})
// 定义字典
const dicData = reactive({});
const onLoad = onLoadDicUrl();
const onFormLoaded = onFormLoadedUrl({key: "users", field: "whoseLeader"}, {key: "depts", field: "appointDeptId"});
onMounted(async () => {
// await onLoad(dicData);
methods.changeTabPane(props.currSelect)
})
function remoteMethodDept(query: string) {
remoteMethodByKey(query, onLoad, dicData, 'deptName', "depts")
}
async function remoteMethod(query: string) {
await remoteMethodByKey(query, onLoad, dicData, 'userName', "users")
revParseWhoseLeaderName(data, dicData)
}
const methods = {
handleUserKeyValFrom(type) {
if (type !== '4' || !validateNull(data.formFieldPerms)) return
validateListFormOption(data, props, $message)
},
confirmMethods() {
parseUserKeyValName(props, data, methods)
$emit("openFlowMethods", false);
},
$message(type) {
if (type === 'whoseLeader') $message.warning('请选择谁的主管');
if (type === 'appointDeptId') $message.warning('请选择指定部门');
},
changeWhoseLeader(userId) {
if (!userId) {
data.whoseLeaderName = '请先指定'
return
}
data.whoseLeaderName = dicData.users.find(f => f.userId === userId).name;
},
validateNodeJobCurrJob() {
return !validateNull(props.currSelect.attributes) && props.currSelect.attributes.attrs.cdata.defJob
&& !validateNull(props.currSelect.attributes.attrs.cdata.defJob.currRunJobs);
},
async changeTabPane(val) {
if (Object.keys(val).length === 0) {
data.activeKey = ''
return
}
// 反解析出已配置项
await revParseUserKeyValName(props, data, dicData, methods)
await onFormLoaded(dicData, data)
revParseWhoseLeaderName(data, dicData)
},
}
// 监听双向绑定
watch(
() => props.currSelect,
(val) => {
if (validateNull(val) || Object.keys(val).length === 0) {
data.activeKey = ''
return
}
methods.changeTabPane(val)
}
);
</script>
<style lang="scss">
@import '../assets/style/flow-attr.scss';
</style>

View File

@@ -0,0 +1,810 @@
<template>
<div>
<el-tabs class="flow-attr"
v-model="data.activeKey">
<el-tab-pane name="flow-rule">
<template #label>
<div>
<el-icon style="vertical-align: middle;margin-right: 3px">
<Setting/>
</el-icon>
<span style="vertical-align: middle;">设置路由规则</span>
</div>
</template>
<el-form label-position="left" class="flow-config-attr" label-width="150px">
<div style="margin: 5px 15px"><span
style="color: red;font-size: 14px">:可根据接口返回值 表单数据的字段值配置对应的路由动作可自定义随意扩展</span>
</div>
<el-form-item label="条件组关系">
<el-switch
v-model="data.cond.groupsType" @change="methods.changeGroupsType"
active-value="0"
inactive-value="1"
inactive-text=""
active-text="" disabled>
</el-switch>
<el-tooltip placement="top">
<template #content>一个组决定一个路由动作多条件组满足时只有开启下一节点驳回到其他节点可以累加路由动作</template>
<el-icon style="margin-left: 10px">
<QuestionFilled/>
</el-icon>
</el-tooltip>
</el-form-item>
<el-form-item label="路由规则模式">
<el-radio-group style="width: 313px" @change="methods.handleCondValType" :disabled="data.existData"
v-model="data.cond.valType">
<el-radio v-for="(item, index) in [DIC_PROP.VAL_TYPE[2], DIC_PROP.VAL_TYPE[4], DIC_PROP.VAL_TYPE[5]]" :key="index" :label="item.value"
style="width: 75px">
{{ item.label }}
</el-radio>
</el-radio-group>
<el-tooltip placement="top">
<template #content>若无法切换模式请先清空条件组列表</template>
<el-icon>
<QuestionFilled/>
</el-icon>
</el-tooltip>
</el-form-item>
<el-divider>组内条件配置</el-divider>
<template v-if="data.cond.valType === '0'">
<el-form-item label="组内条件关系">
<el-switch
v-model="data.cond.groupType"
active-value="0"
inactive-value="1"
inactive-text=""
active-text="">
</el-switch>
</el-form-item>
<el-table :data="data.cond.condGroup"
border style="width: 100%; margin-bottom: 10px" max-height="500">
<el-table-column type="index" label="操作" width="80">
<template #header>
<el-button icon="Plus" size="small" type="primary" circle @click="methods.onAddItem(true)"></el-button>
</template>
<template #default="scope">
<el-button icon="Minus" size="small" type="danger" circle @click="methods.handleCondDelete(scope.$index, scope.row)"></el-button>
</template>
</el-table-column>
<el-table-column prop="varKeyVal" :label="t('flowClazz.varKeyVal')" show-overflow-tooltip>
<template #default="scope">
<el-select v-model="scope.row.varKeyVal" @change="methods.handleVarKeyVal(scope.row)"
clearable
filterable>
<el-option-group
v-for="(group, index) in data.allFieldPerms"
:key="index"
:label="group.label">
<el-option
v-for="(item, index) in group.options"
:disabled="item.prop.indexOf('_define_') !== -1"
:key="index"
:label="item.label"
:value="item.prop">
</el-option>
</el-option-group>
</el-select>
</template>
</el-table-column>
<el-table-column prop="operator" :label="t('flowClazz.operator')" show-overflow-tooltip>
<template #default="scope">
<el-select v-model="scope.row.operator" @change="methods.handleVarKeyVal(scope.row)"
clearable>
<el-option
v-for="(item, index) in data.varValOperator"
:key="index"
:label="item.label"
:value="item.value">
</el-option>
</el-select>
</template>
</el-table-column>
<el-table-column prop="varVal" :label="t('flowClazz.varVal')" show-overflow-tooltip>
<template #default="scope">
<el-input v-model="scope.row.varVal" clearable/>
</template>
</el-table-column>
</el-table>
<el-form-item label="路由动作">
<el-select v-model="data.cond.routeAction" @change="methods.changeRouteAction(data.cond)">
<el-option v-for="(item, index) in DIC_PROP.ROUTE_ACTIONS" :key="index" :disabled="item.value.indexOf('_define_') !== -1"
:label="item.label" :value="item.value"></el-option>
</el-select>
</el-form-item>
<el-form-item label="路由到哪个节点" v-if="data.cond.routeAction === DIC_PROP.ROUTE_ACTIONS[1].value">
<el-select class="input-attr" v-model="data.cond.toFlowNodeId"
clearable filterable>
<template v-for="(item, index) in data.toFlowNodeIds" :key="index">
<el-option :label="item.nodeName"
:value="item.id">
</el-option>
</template>
</el-select>
<el-tooltip placement="top" content="组内条件满足时,路由到哪个节点(注意:若节点参与者为空,可指定默认的参与者)">
<el-icon style="margin-left: 10px"><QuestionFilled /></el-icon>
</el-tooltip>
</el-form-item>
<template v-if="data.cond.routeAction === DIC_PROP.ROUTE_ACTIONS[1].value">
<el-form-item label="参与者类型">
<el-tooltip placement="top" content="若节点参与者为空,可指定默认的参与者">
<el-select v-model="data.cond.jobType" @change="methods.changeJobType(data.cond)" clearable>
<el-option v-for="(item, index) in DIC_PROP.JOB_USER_TYPE" :key="index" :label="item.label" :value="item.value"></el-option>
</el-select>
</el-tooltip>
</el-form-item>
<el-form-item label="哪个人来审批" v-if="data.cond.jobType === DIC_PROP.JOB_USER_TYPE[0].value">
<el-tooltip content="请输入用户名称进行模糊搜索" placement="top">
<el-select class="input-attr"
v-model="data.cond.roleId"
clearable filterable
remote :remote-method="methodsRemote.remoteMethodUser2" :reserve-keyword="false">
<template v-for="(item, index) in dicData.users2" :key="index">
<el-option :label="item.name"
:value="item.userId">
</el-option>
</template>
</el-select>
</el-tooltip>
<el-tooltip placement="top" content="组内条件满足时,由哪个人来审批">
<el-icon style="margin-left: 10px"><QuestionFilled /></el-icon>
</el-tooltip>
</el-form-item>
<el-form-item label="哪个角色审批" v-if="data.cond.jobType === DIC_PROP.JOB_USER_TYPE[1].value">
<el-tooltip content="请输入角色名称进行模糊搜索" placement="top">
<el-select class="input-attr" v-model="data.cond.roleId"
filterable
clearable
remote :remote-method="methodsRemote.remoteMethodRole2" :reserve-keyword="false">
<el-option
v-for="(item, index) in dicData.roles2"
:key="index"
:label="item.roleName"
:value="item.roleId">
</el-option>
</el-select>
</el-tooltip>
<el-tooltip placement="top" content="组内条件满足时,由哪个角色来审批">
<el-icon style="margin-left: 10px"><QuestionFilled /></el-icon>
</el-tooltip>
</el-form-item>
<el-form-item label="哪个岗位审批" v-if="data.cond.jobType === DIC_PROP.JOB_USER_TYPE[2].value">
<el-tooltip content="请输入岗位名称进行模糊搜索" placement="top">
<el-select class="input-attr" v-model="data.cond.roleId"
filterable
clearable
remote :remote-method="methodsRemote.remoteMethodPost2" :reserve-keyword="false">
<el-option
v-for="(item, index) in dicData.posts2"
:key="index"
:label="item.postName"
:value="item.postId">
</el-option>
</el-select>
</el-tooltip>
<el-tooltip placement="top" content="组内条件满足时,由哪个岗位来审批">
<el-icon style="margin-left: 10px"><QuestionFilled /></el-icon>
</el-tooltip>
</el-form-item>
<el-form-item label="哪个部门审批" v-if="data.cond.jobType === DIC_PROP.JOB_USER_TYPE[3].value">
<el-tooltip content="请输入部门名称进行模糊搜索" placement="top">
<el-select class="input-attr" v-model="data.cond.roleId"
filterable
clearable
remote :remote-method="methodsRemote.remoteMethodDept2" :reserve-keyword="false">
<el-option
v-for="(item, index) in dicData.depts2"
:key="index"
:label="item.name"
:value="item.deptId">
</el-option>
</el-select>
</el-tooltip>
<el-tooltip placement="top" content="组内条件满足时,由哪个部门审批">
<el-icon style="margin-left: 10px"><QuestionFilled /></el-icon>
</el-tooltip>
</el-form-item>
</template>
<el-form-item label="驳回到哪个节点" v-if="data.cond.routeAction === DIC_PROP.ROUTE_ACTIONS[3].value">
<el-select class="input-attr" v-model="data.cond.toFlowNodeId"
clearable filterable>
<template v-for="(item, index) in data.toFlowNodeIds" :key="index">
<el-option :label="item.nodeName"
:value="item.id">
</el-option>
</template>
</el-select>
<el-tooltip placement="top" content="组内条件满足时,驳回到哪个节点(注意:必须是已审批过的节点)">
<el-icon style="margin-left: 10px"><QuestionFilled /></el-icon>
</el-tooltip>
</el-form-item>
</template>
<el-form-item label="函数表达式" v-if="data.cond.valType === '2'">
<el-input class="input-attr" v-model="data.cond.varKeyVal" placeholder="请输入函数表达式" clearable @blur="methods.handleCondVarKeyVal"/>
<el-tooltip placement="top">
<template #content>采用表达式取值 ( 以下两种方式均支持自定义任意扩展 ), 值可以为对象 数组, 满足您复杂路由的场景:
<br />{{ PROP_CONST.TEXT_DESC.condUserExplain }}
<br />{{ PROP_CONST.TEXT_DESC.condMethodExplain3 }}, 返回值可以为对象 数组{{ PROP_CONST.TEXT_DESC.condMethodExplain6 }}
<br />{{ PROP_CONST.TEXT_DESC.condMethodExplain4 }}
</template>
<el-icon style="margin-left: 10px">
<QuestionFilled/>
</el-icon>
</el-tooltip>
</el-form-item>
<div v-if="data.cond.valType === '2'" style="margin: 10px 13px">
<span style="color: #409EFF;font-size: 14px">: 当前函数表达式的返回值可以为对象 数组{{ PROP_CONST.TEXT_DESC.condMethodExplain6 }}</span>
</div>
<template v-if="data.cond.valType === '3'">
<el-form-item label="Http请求地址">
<el-input class="input-attr" placeholder="可输入全路径或相对路径" v-model="data.cond.varKeyVal"
@blur="methods.handleCondVarKeyVal" clearable>
<template #prepend>
<el-select v-model="data.cond.httpMethod">
<el-option v-for="(item, index) in DIC_PROP.HTTP_METHODS" :key="index" :label="item.label" :value="item.value"></el-option>
</el-select>
</template>
</el-input>
<el-tooltip content="点击可设置Http请求的参数信息" placement="bottom">
<el-button type="primary" size="small" round style="margin-left: 10px"
@click="methods.openHttpUrlParams()">
{{ data.httpUrlParamsVisible ? '清空' : '参数' }}
</el-button>
</el-tooltip>
</el-form-item>
<flow-http-param ref="flowHttpParam" :currFlowForm="props.currFlowForm" :flowData="props.flowData"
:httpParam="props.currSelect.attributes.attrs.cdata.attrs"
:httpParamType="DIC_PROP.PARAM_RULE_TYPE[8].value"
v-if="data.httpUrlParamsVisible"></flow-http-param>
</template>
<template v-if="data.cond.valType === '0'">
<el-button type="primary" round style="margin-left: 185px; margin-top: 30px; margin-bottom: 30px; width: 200px" @click="methods.addFlowNodeCondGroup()">
{{ data.oldCurrentRow ? '修改完成': '添加条件组' }}
</el-button>
<el-divider>已添加条件组列表(点击行可再次修改)</el-divider>
<el-empty description="条件组列表为空" style="margin: 10px 230px" v-if="!data.existData">
</el-empty>
<template v-for="(item, index) in data.condGroups" v-else
:key="index">
<el-collapse v-model="data.collapse">
<el-collapse-item :name="index">
<template #title>
{{ '条件组 ' + (index + 1) }}
<el-icon style="margin-left: 10px" @click="methods.handleCondGroupDelete(index)">
<Delete />
</el-icon>
</template>
<el-form-item label="组内条件关系">
<el-switch
v-model="item.groupType" @change="methods.changeGroupType(item, index)"
active-value="0"
inactive-value="1"
inactive-text=""
active-text="">
</el-switch>
</el-form-item>
<el-table :data="item.condGroup" border style="width: 100%; margin-bottom: 10px" max-height="500"
highlight-current-row @current-change="methods.handleCurrentChange" :ref="'tableDataRef' + index">
<el-table-column type="index" label="操作" width="80">
<template #default="scope">
<el-button icon="Minus" size="small" type="danger" circle
@click="methods.handleSingleCondDelete(scope.$index, scope.row, index)"></el-button>
</template>
</el-table-column>
<el-table-column prop="varKeyVal" :label="t('flowClazz.varKeyVal')" show-overflow-tooltip>
<template #default="scope">
<convert-group-name :options="data.allFieldPerms"
:value="scope.row.varKeyVal"
:valueKey="'prop'" :showKey="'label'"></convert-group-name>
</template>
</el-table-column>
<el-table-column prop="operator" :label="t('flowClazz.operator')" show-overflow-tooltip>
<template #default="scope">
<dict-tag :options="DIC_PROP.OPERATOR" :value="scope.row.operator"></dict-tag>
</template>
</el-table-column>
<el-table-column prop="varVal" :label="t('flowClazz.varVal')" show-overflow-tooltip/>
</el-table>
<el-form-item label="路由动作">
<el-select v-model="item.routeAction" @change="methods.changeRouteAction(item)">
<el-option v-for="(item, index) in DIC_PROP.ROUTE_ACTIONS" :key="index" :disabled="item.value.indexOf('_define_') !== -1"
:label="item.label" :value="item.value"></el-option>
</el-select>
</el-form-item>
<el-form-item label="路由到哪个节点" v-if="item.routeAction === DIC_PROP.ROUTE_ACTIONS[1].value">
<el-select class="input-attr" v-model="item.toFlowNodeId"
clearable filterable>
<template v-for="(item, index) in data.toFlowNodeIds" :key="index">
<el-option :label="item.nodeName"
:value="item.id">
</el-option>
</template>
</el-select>
<el-tooltip placement="top" content="组内条件满足时,路由到哪个节点(注意:若节点参与者为空,可指定默认的参与者)">
<el-icon style="margin-left: 10px"><QuestionFilled /></el-icon>
</el-tooltip>
</el-form-item>
<template v-if="item.routeAction === DIC_PROP.ROUTE_ACTIONS[1].value">
<el-form-item label="参与者类型">
<el-tooltip placement="top" content="若节点参与者为空,可指定默认的参与者">
<el-select v-model="item.jobType" @change="methods.changeJobType(item)" clearable>
<el-option v-for="(item, index) in DIC_PROP.JOB_USER_TYPE" :key="index" :label="item.label" :value="item.value"></el-option>
</el-select>
</el-tooltip>
</el-form-item>
<el-form-item label="哪个人来审批" v-if="item.jobType === DIC_PROP.JOB_USER_TYPE[0].value">
<el-tooltip content="请输入用户名称进行模糊搜索" placement="top">
<el-select class="input-attr"
v-model="item.roleId"
clearable filterable
remote :remote-method="methodsRemote.remoteMethodUser" :reserve-keyword="false">
<template v-for="(item, index) in dicData.users" :key="index">
<el-option :label="item.name"
:value="item.userId">
</el-option>
</template>
</el-select>
</el-tooltip>
<el-tooltip placement="top" content="组内条件满足时,由哪个人来审批">
<el-icon style="margin-left: 10px"><QuestionFilled /></el-icon>
</el-tooltip>
</el-form-item>
<el-form-item label="哪个角色审批" v-if="item.jobType === DIC_PROP.JOB_USER_TYPE[1].value">
<el-tooltip content="请输入角色名称进行模糊搜索" placement="top">
<el-select class="input-attr" v-model="item.roleId"
filterable
clearable
remote :remote-method="methodsRemote.remoteMethodRole" :reserve-keyword="false">
<el-option
v-for="(item, index) in dicData.roles"
:key="index"
:label="item.roleName"
:value="item.roleId">
</el-option>
</el-select>
</el-tooltip>
<el-tooltip placement="top" content="组内条件满足时,由哪个角色来审批">
<el-icon style="margin-left: 10px"><QuestionFilled /></el-icon>
</el-tooltip>
</el-form-item>
<el-form-item label="哪个岗位审批" v-if="item.jobType === DIC_PROP.JOB_USER_TYPE[2].value">
<el-tooltip content="请输入岗位名称进行模糊搜索" placement="top">
<el-select class="input-attr" v-model="item.roleId"
filterable
clearable
remote :remote-method="methodsRemote.remoteMethodPost" :reserve-keyword="false">
<el-option
v-for="(item, index) in dicData.posts"
:key="index"
:label="item.postName"
:value="item.postId">
</el-option>
</el-select>
</el-tooltip>
<el-tooltip placement="top" content="组内条件满足时,由哪个岗位来审批">
<el-icon style="margin-left: 10px"><QuestionFilled /></el-icon>
</el-tooltip>
</el-form-item>
<el-form-item label="哪个部门审批" v-if="item.jobType === DIC_PROP.JOB_USER_TYPE[3].value">
<el-tooltip content="请输入部门名称进行模糊搜索" placement="top">
<el-select class="input-attr" v-model="item.roleId"
filterable
clearable
remote :remote-method="methodsRemote.remoteMethodDept" :reserve-keyword="false">
<el-option
v-for="(item, index) in dicData.depts"
:key="index"
:label="item.name"
:value="item.deptId">
</el-option>
</el-select>
</el-tooltip>
<el-tooltip placement="top" content="组内条件满足时,由哪个部门审批">
<el-icon style="margin-left: 10px"><QuestionFilled /></el-icon>
</el-tooltip>
</el-form-item>
</template>
<el-form-item label="驳回到哪个节点" v-if="item.routeAction === DIC_PROP.ROUTE_ACTIONS[3].value">
<el-select class="input-attr" v-model="item.toFlowNodeId"
clearable filterable>
<template v-for="(item, index) in data.toFlowNodeIds" :key="index">
<el-option :label="item.nodeName"
:value="item.id">
</el-option>
</template>
</el-select>
<el-tooltip placement="top" content="组内条件满足时,驳回到哪个节点(注意:必须是已审批过的节点)">
<el-icon style="margin-left: 10px"><QuestionFilled /></el-icon>
</el-tooltip>
</el-form-item>
</el-collapse-item>
</el-collapse>
</template>
</template>
</el-form>
</el-tab-pane>
</el-tabs>
</div>
</template>
<script setup lang="ts" name="FlowMethod">
import {useI18n} from "vue-i18n";
import {useMessage} from "/@/hooks/message";
import {rule, validateNull} from "/@/utils/validate";
import {deepClone} from "/@/utils/other";
import {DIC_PROP} from "/@/flow/support/dict-prop";
import {buildSysFieldsFormOption} from "../../utils/form-perm";
import {
onFormLoadedUrl,
onLoadDicUrl,
initRemoteMethodAllByKey
} from "../../components/convert-name/convert";
import {PROP_CONST} from "../../support/prop-const";
import {handleChangeJobType} from "../../index";
import {validateNodeType} from "./index";
const { proxy } = getCurrentInstance();
const FlowHttpParam = defineAsyncComponent(() => import('./flow-http-param.vue'));
const $emit = defineEmits(["openFlowRoutes"]);
const {t} = useI18n();
const $message = useMessage();
const props = defineProps({
currFlowForm: {
type: Object,
default: null,
},
currSelect: {
type: Object,
default: {},
},
flowData: {
type: Object,
default: null,
},
});
const condData = {
httpMethod: 'GET',
roleId: null,
jobType: null,
routeAction: null,
toFlowNodeId: null,
groupsType: '1',
condGroup: [],
groupType: '0',
valType: '0',
varKeyVal: null,
operator: null,
varVal: null,
}
const data = reactive({
activeKey: 'flow-rule',
collapse: [0],
allFieldPerms: [],
formFieldPerms: [],
cond: deepClone(condData),
httpUrlParamsVisible: false,
oldCurrentRow: null,
condGroups: [],
toFlowNodeIds: [],
existData: false,
})
// 定义字典
const dicData = reactive({});
const onLoad = onLoadDicUrl();
const onFormLoaded = onFormLoadedUrl(...PROP_CONST.LOAD_USER_ROLE);
onMounted(async () => {
// await onLoad(dicData);
methods.changeTabPane(props.currSelect)
})
const methodsRemote = initRemoteMethodAllByKey(onLoad, dicData)
const methods = {
changeJobType(item) {
handleChangeJobType(dicData, item)
},
onFormLoaded() {
let condGroups = props.currSelect.attributes.attrs.cdata.attrs.condGroups
if (validateNull(condGroups)) return
onFormLoaded(dicData, condGroups)
},
changeRouteAction(item) {
// 切换清空
let b = item.routeAction === DIC_PROP.ROUTE_ACTIONS[1].value;
if(!b && item.routeAction !== DIC_PROP.ROUTE_ACTIONS[3].value) {
data.cond.toFlowNodeId = null
}
},
openHttpUrlParams() {
if (!data.cond.varKeyVal) {
$message.warning("请先输入【Http请求地址】")
return
}
if (data.httpUrlParamsVisible) {
props.currSelect.attributes.attrs.cdata.attrs.httpParams = []
data.httpUrlParamsVisible = false
} else {
data.httpUrlParamsVisible = true
}
},
handleCurrentChange(row) {
if (!row) return
let condGroupsRow;
for (let i = 0; i < data.condGroups.length; i++) {
let index = data.condGroups[i].condGroup.indexOf(row);
if (index !== -1) {
condGroupsRow = data.condGroups[i];
}
}
// 先清空之前选中的其他条件组
if (data.oldCurrentRow !== condGroupsRow) {
let oldIndex = data.condGroups.indexOf(data.oldCurrentRow);
if (oldIndex !== -1) {
proxy.$refs['tableDataRef' + oldIndex][0].setCurrentRow(null)
}
}
data.cond = condGroupsRow
data.oldCurrentRow = condGroupsRow
},
handleVarKeyVal(row) {
let dots = row.varKeyVal.split('.').length - 1;
if (dots === 2) {
let find = DIC_PROP.OPERATOR.slice(6).find(f => f.value === row.operator);
if (!find && row.operator) {
$message.warning("子表单的字段只能选择包含或不包含")
row.operator = null
}
}
},
changeGroupsType(groupsType) {
let condGroups = props.currSelect.attributes.attrs.cdata.attrs.condGroups;
if (validateNull(condGroups)) return
props.currSelect.attributes.attrs.cdata.attrs.condGroups.forEach(each => {
each.groupsType = groupsType
})
methods.validateCondData()
},
changeGroupType(item, index) {
let condGroups = props.currSelect.attributes.attrs.cdata.attrs.condGroups;
if (validateNull(condGroups)) return
props.currSelect.attributes.attrs.cdata.attrs.condGroups[index].groupType = item.groupType
methods.validateCondData()
},
onAddItem(isAdd) {
if (validateNull(data.formFieldPerms)) {
buildSysFieldsFormOption(data, props, $message)
}
if (data.cond.condGroup.length > 0) {
let find = data.cond.condGroup.find(f => !f.varKeyVal || !f.operator || !f.varVal);
if (find) {
let b = !find.varKeyVal || !find.operator || !find.varVal;
if (isAdd && b) {
$message.warning("请先填写 表单字段 或 运算符 或 值")
return
}
}
if (!isAdd) data.cond.condGroup.splice(0, data.cond.condGroup.length);
}
let obj = {varKeyVal: '', operator: '', varVal: ''};
data.cond.condGroup.push(obj);
},
addFlowNodeCondGroup() {
if (validateNull(data.cond.condGroup)) {
$message.warning("请先添加组内条件")
return
}
let valType = data.cond.valType;
let find = data.cond.condGroup.find(f => !f.varKeyVal || !f.operator || !f.varVal);
if (find) {
if (valType === DIC_PROP.VAL_TYPE[2].value) {
if (!find.varKeyVal || !find.operator || !find.varVal) {
$message.warning("表单字段 或 运算符 或 值 不能为空")
return
}
}
}
if (!methods.validateCondGroupAttrs()) return;
if (data.oldCurrentRow) {
// 先清空之前选中
let index = data.condGroups.indexOf(data.cond);
if (index !== -1) {
proxy.$refs['tableDataRef' + index][0].setCurrentRow(null)
}
data.oldCurrentRow = null
data.cond = deepClone(condData);
} else {
let cond = deepClone(data.cond);
props.currSelect.attributes.attrs.cdata.attrs.condGroups.push(cond)
methods.validateCondData()
methods.updateCondVarKeyVal(true)
}
methods.onFormLoaded()
},
validateCondGroupAttrs() {
if (!data.cond.routeAction) {
$message.warning("路由动作 不能为空")
return false
}
// 校验动作类型参数
let b = data.cond.routeAction === DIC_PROP.ROUTE_ACTIONS[1].value;
if(b || data.cond.routeAction === DIC_PROP.ROUTE_ACTIONS[3].value) {
if (!data.cond.toFlowNodeId) {
$message.warning("路由到哪个节点 不能为空")
return false
}
}
return true
},
handleCondGroupDelete(index: number) {
props.currSelect.attributes.attrs.cdata.attrs.condGroups.splice(index, 1)
methods.validateCondData()
},
handleSingleCondDelete(index: number, row: any, groupIndex) {
props.currSelect.attributes.attrs.cdata.attrs.condGroups[groupIndex].condGroup.splice(index, 1)
if (validateNull(props.currSelect.attributes.attrs.cdata.attrs.condGroups[groupIndex].condGroup)) {
methods.handleCondGroupDelete(groupIndex)
}
methods.validateCondData()
},
handleCondDelete(index: number, row: any) {
data.cond.condGroup.splice(index, 1)
},
handleCondValType(type?) {
if (type) {
data.cond.varKeyVal = null
methods.updateCondVarKeyVal(false)
}
if (!type) {
type = methods.initCurrVarKeyVal()
}
if (type === DIC_PROP.VAL_TYPE[2].value) {
data.varValOperator = DIC_PROP.OPERATOR
methods.onAddItem(false)
} else {
data.varValOperator = []
}
data.cond.operator = null
data.cond.varVal = null
},
handleCondVarKeyVal() {
let val = data.cond.varKeyVal
let valType = data.cond.valType;
if (!val) {
methods.updateCondVarKeyVal(false)
return
}
if (valType === DIC_PROP.VAL_TYPE[2].value) return
if (valType === DIC_PROP.VAL_TYPE[4].value && val.indexOf("#") === -1) {
data.cond.varKeyVal = null
$message.warning("当选择专业模式时, 函数表达式必须符合规定的格式")
return;
}
methods.updateCondVarKeyVal(true)
},
updateCondVarKeyVal(isSave) {
props.currSelect.attributes.attrs.cdata.attrs.valType = data.cond.valType
if (isSave) {
if (data.cond.valType === DIC_PROP.VAL_TYPE[2].value) {
props.currSelect.attributes.attrs.cdata.attrs.routeKeyVal = PROP_CONST.VAR_KEY_VAL.route
props.currSelect.attributes.attrs.cdata.attrs.routeKeyValName = PROP_CONST.VAR_KEY_VAL.routeName
} else {
props.currSelect.attributes.attrs.cdata.attrs.routeKeyVal = data.cond.varKeyVal
}
props.currSelect.attributes.attrs.cdata.attrs.httpMethod = data.cond.httpMethod
} else {
props.currSelect.attributes.attrs.cdata.attrs.routeKeyVal = null
props.currSelect.attributes.attrs.cdata.attrs.routeKeyValName = null
props.currSelect.attributes.attrs.cdata.attrs.condGroups = []
props.currSelect.attributes.attrs.cdata.attrs.httpParams = []
props.currSelect.attributes.attrs.cdata.attrs.httpMethod = null
data.httpUrlParamsVisible = false
}
},
validateCondData() {
let condGroups = props.currSelect.attributes.attrs.cdata.attrs.condGroups;
if (!condGroups) {
props.currSelect.attributes.attrs.cdata.attrs.condGroups = []
}
data.condGroups.splice(0, data.condGroups.length);
props.currSelect.attributes.attrs.cdata.attrs.condGroups.forEach(each => data.condGroups.push(each))
data.existData = !validateNull(data.condGroups)
},
initCurrVarKeyVal() {
data.cond.valType = props.currSelect.attributes.attrs.cdata.attrs.valType
if (data.condGroups.length <= 0) {
data.cond.varKeyVal = props.currSelect.attributes.attrs.cdata.attrs.routeKeyVal
let httpMethod = props.currSelect.attributes.attrs.cdata.attrs.httpMethod
if (httpMethod) data.cond.httpMethod = httpMethod
}
let httpParams = props.currSelect.attributes.attrs.cdata.attrs.httpParams;
if (!validateNull(httpParams)) {
data.httpUrlParamsVisible = true
}
return data.cond.valType
},
handleFlowNodeIds() {
data.toFlowNodeIds = []
let models = window._jfGraph.getElements();
if (validateNull(models)) return
models.forEach(each => {
if (!validateNodeType(each)) return
if (props.currSelect.id !== each.id) {
let id = each.id
let nodeName = each.attributes.attrs.label.text
data.toFlowNodeIds.push({id, nodeName})
}
})
},
changeTabPane(val) {
methods.validateCondData()
methods.handleCondValType()
methods.handleFlowNodeIds()
methods.onFormLoaded()
}
}
// 监听双向绑定
watch(
() => props.currSelect,
(val) => {
if (Object.keys(val).length === 0) {
return
}
methods.changeTabPane(val)
}
);
</script>
<style lang="scss">
@import '../assets/style/flow-attr.scss';
</style>

View File

@@ -0,0 +1,361 @@
<template>
<el-form label-position="left" class="flow-attr flow-param-attr" label-width="200px">
<el-divider>关联子流程Http接口</el-divider>
<el-form-item label="保存子流程表单Http请求地址">
<el-input class="input-attr" placeholder="可输入全路径或相对路径" v-model="props.currSelect.attributes.attrs.cdata.attrs.startSubFlow"
clearable>
<template #prepend>
<el-select v-model="props.currSelect.attributes.attrs.cdata.attrs.startSubMethod">
<el-option v-for="(item, index) in DIC_PROP.HTTP_METHODS" :key="index" :label="item.label" :value="item.value"></el-option>
</el-select>
</template>
</el-input>
<el-tooltip placement="top">
<template #content>启动子流程时更新子流程表单接口可输入全路径或相对路径</template>
<el-icon style="margin-left: 10px">
<QuestionFilled/>
</el-icon>
</el-tooltip>
</el-form-item>
<el-form-item label="更新子流程表单Http请求地址">
<el-input class="input-attr" placeholder="可输入全路径或相对路径" v-model="props.currSelect.attributes.attrs.cdata.attrs.restartSubFlow"
clearable>
<template #prepend>
<el-select v-model="props.currSelect.attributes.attrs.cdata.attrs.restartSubMethod">
<el-option v-for="(item, index) in DIC_PROP.HTTP_METHODS" :key="index" :label="item.label" :value="item.value"></el-option>
</el-select>
</template>
</el-input>
<el-tooltip placement="top">
<template #content>重入或重启子流程时更新子流程表单接口可输入全路径或相对路径</template>
<el-icon style="margin-left: 10px">
<QuestionFilled/>
</el-icon>
</el-tooltip>
</el-form-item>
<el-form-item label="更新父流程表单Http请求地址">
<el-input class="input-attr" placeholder="可输入全路径或相对路径" v-model="props.currSelect.attributes.attrs.cdata.attrs.backParFlow"
clearable>
<template #prepend>
<el-select v-model="props.currSelect.attributes.attrs.cdata.attrs.backParMethod">
<el-option v-for="(item, index) in DIC_PROP.HTTP_METHODS" :key="index" :label="item.label" :value="item.value"></el-option>
</el-select>
</template>
</el-input>
<el-tooltip placement="top">
<template #content>返回父流程时更新父流程表单接口可输入全路径或相对路径</template>
<el-icon style="margin-left: 10px">
<QuestionFilled/>
</el-icon>
</el-tooltip>
</el-form-item>
<el-divider>关联子流程Http请求头</el-divider>
<el-table :data="data.subFlowParams.filter(f => f.paramFrom === DIC_PROP.PARAM_FROM[0].value)"
border style="width: 100%; margin-bottom: 10px" max-height="500">
<el-table-column type="index" :label="t('jfI18n.operate')" width="80">
<template #header>
<el-button icon="Plus" size="small" type="primary" circle
@click="methods.onAddItem(DIC_PROP.PARAM_FROM[0].value, true)"></el-button>
</template>
<template #default="scope">
<el-button icon="Minus" size="small" type="danger" circle
@click="methods.handleSubFlowDelete(scope.$index, scope.row)"></el-button>
</template>
</el-table-column>
<el-table-column prop="targetProp" :label="t('jfAttr.requestHeader')" show-overflow-tooltip>
<template #default="scope">
<el-input v-model="scope.row.targetProp" :placeholder="scope.row.paramValType === DIC_PROP.PARAM_VAL_TYPE[0].value ? scope.row.varKeyVal : null"
clearable></el-input>
</template>
</el-table-column>
<el-table-column prop="paramValType" :label="t('jfAttr.paramValType')" width="145px">
<template #default="scope">
<el-select v-model="scope.row.paramValType" clearable>
<el-option v-for="(item, index) in DIC_PROP.PARAM_VAL_TYPE" :key="index" :label="item.label" :value="item.value"></el-option>
</el-select>
</template>
</el-table-column>
<el-table-column prop="varKeyVal" :label="t('jfAttr.varKeyVal')" show-overflow-tooltip>
<template #default="scope">
<el-select v-model="scope.row.varKeyVal"
v-if="scope.row.paramValType === DIC_PROP.PARAM_VAL_TYPE[0].value"
clearable filterable>
<el-option-group
v-for="(group, index) in data.allFieldPerms"
:key="index"
:label="group.label">
<el-option
v-for="(item, index) in group.options"
:disabled="item.prop.indexOf('_define_') !== -1"
:key="index"
:label="item.label"
:value="item.prop">
</el-option>
</el-option-group>
</el-select>
<el-input v-else v-model="scope.row.varKeyVal" clearable> </el-input>
</template>
</el-table-column>
</el-table>
<el-divider>父流程传参到子流程</el-divider>
<el-table :data="data.subFlowParams.filter(f => f.paramFrom === DIC_PROP.PARAM_FROM[1].value)"
border style="width: 100%; margin-bottom: 10px" max-height="500">
<el-table-column type="index" :label="t('jfI18n.operate')" width="80">
<template #header>
<el-button icon="Plus" size="small" type="primary" circle
@click="methods.onAddItem(DIC_PROP.PARAM_FROM[1].value, true)"></el-button>
</template>
<template #default="scope">
<el-button icon="Minus" size="small" type="danger" circle
@click="methods.handleSubFlowDelete(scope.$index, scope.row)"></el-button>
</template>
</el-table-column>
<el-table-column prop="varKeyVal" :label="t('jfAttr.varKeyVal')" show-overflow-tooltip>
<template #default="scope">
<el-select v-model="scope.row.varKeyVal"
v-if="scope.row.paramValType === DIC_PROP.PARAM_VAL_TYPE[0].value"
clearable filterable>
<el-option-group
v-for="(group, index) in data.allFieldPerms"
:key="index"
:label="group.label">
<el-option
v-for="(item, index) in group.options"
:disabled="item.prop.indexOf('_define_') !== -1"
:key="index"
:label="item.label"
:value="item.prop">
</el-option>
</el-option-group>
</el-select>
<el-input v-else v-model="scope.row.varKeyVal" clearable> </el-input>
</template>
</el-table-column>
<el-table-column prop="paramValType" :label="t('jfAttr.paramValType')" width="145px">
<template #default="scope">
<el-select v-model="scope.row.paramValType" clearable>
<el-option v-for="(item, index) in DIC_PROP.PARAM_VAL_TYPE" :key="index" :label="item.label" :value="item.value"></el-option>
</el-select>
</template>
</el-table-column>
<el-table-column prop="targetProp" :label="t('jfAttr.subVarKeyVal')" show-overflow-tooltip>
<template #default="scope">
<el-input v-model="scope.row.targetProp" :placeholder="scope.row.paramValType === DIC_PROP.PARAM_VAL_TYPE[0].value ? scope.row.varKeyVal : null"
clearable></el-input>
</template>
</el-table-column>
</el-table>
<el-divider>子流程回参到父流程</el-divider>
<el-table :data="data.subFlowParams.filter(f => f.paramFrom === DIC_PROP.PARAM_FROM[2].value)"
border style="width: 100%; margin-bottom: 10px" max-height="500">
<el-table-column type="index" :label="t('jfI18n.operate')" width="80">
<template #header>
<el-button icon="Plus" size="small" type="primary" circle
@click="methods.onAddItem(DIC_PROP.PARAM_FROM[2].value, true)"></el-button>
</template>
<template #default="scope">
<el-button icon="Minus" size="small" type="danger" circle
@click="methods.handleSubFlowDelete(scope.$index, scope.row)"></el-button>
</template>
</el-table-column>
<el-table-column prop="targetProp" :label="t('jfAttr.subVarKeyVal2')" show-overflow-tooltip>
<template #default="scope">
<el-input v-model="scope.row.targetProp"
:placeholder="scope.row.paramValType === DIC_PROP.PARAM_VAL_TYPE[0].value ? scope.row.varKeyVal : null"
clearable></el-input>
</template>
</el-table-column>
<el-table-column prop="paramValType" :label="t('jfAttr.paramValType')" width="145px">
<template #default="scope">
<el-switch
v-model="scope.row.paramValType"
:active-value="DIC_PROP.PARAM_VAL_TYPE[0].value"
:active-text="DIC_PROP.PARAM_VAL_TYPE[0].label"
:inactive-value="DIC_PROP.PARAM_VAL_TYPE[2].value"
:inactive-text="DIC_PROP.PARAM_VAL_TYPE[2].label"
inline-prompt>
</el-switch>
</template>
</el-table-column>
<el-table-column prop="varKeyVal" :label="t('flowClazz.varKeyVal')" show-overflow-tooltip>
<template #default="scope">
<el-select v-model="scope.row.varKeyVal"
clearable filterable>
<el-option-group
v-for="(group, index) in data.allFieldPerms"
:key="index"
:label="group.label">
<el-option
v-for="(item, index) in group.options"
:disabled="item.prop.indexOf('_define_') !== -1"
:key="index"
:label="item.label"
:value="item.prop">
</el-option>
</el-option-group>
</el-select>
</template>
</el-table-column>
</el-table>
</el-form>
</template>
<script setup lang="ts" name="FlowSubParam">
import {useI18n} from "vue-i18n";
import {useMessage} from "/@/hooks/message";
import {validateNull} from "/@/utils/validate";
import {buildSysFieldsFormOption} from "../../utils/form-perm";
import {DIC_PROP} from "../../support/dict-prop";
const {proxy} = getCurrentInstance();
const {t} = useI18n();
const $message = useMessage();
const props = defineProps({
currFlowForm: {
type: Object,
default: null,
},
currSelect: {
type: Object,
default: null,
},
flowData: {
type: Object,
default: null,
},
});
const data = reactive({
allFieldPerms: [],
formFieldPerms: [],
subFlowParams: [],
})
// 定义字典
onMounted(async () => {
methods.changeTabPane(props.currSelect)
})
const methods = {
listFormFieldPerms() {
if (validateNull(data.formFieldPerms)) {
buildSysFieldsFormOption(data, props, $message)
}
},
onAddItem(paramFrom, isAdd) {
methods.listFormFieldPerms()
let value = DIC_PROP.PARAM_VAL_TYPE[0].value;
if (data.subFlowParams.length > 0) {
let find = data.subFlowParams.filter(f => f.paramFrom === paramFrom).find(f => !f.varKeyVal || (f.paramValType !== value && !f.targetProp));
if (find) {
if (isAdd) {
if (!find.varKeyVal) {
$message.warning("请先填写 表单字段")
return
}
if (find.paramValType !== value && !find.targetProp) {
if (DIC_PROP.PARAM_FROM[0].value === paramFrom) {
$message.warning("请先填写 请求头")
} else {
$message.warning("请先填写 子流程表单字段")
}
return
}
}
}
if (!isAdd) data.subFlowParams.splice(0, data.subFlowParams.length);
}
let obj = {paramFrom: paramFrom, varKeyVal: null, paramValType: value, targetProp: null};
data.subFlowParams.push(obj);
methods.changeSubFlowParams()
},
handleSubFlowDelete(index: number, row: any) {
let splice = data.subFlowParams.filter(f => f.paramFrom === row.paramFrom);
splice.splice(index, 1);
if (DIC_PROP.PARAM_FROM[0].value === row.paramFrom) {
let res = data.subFlowParams.filter(f => f.paramFrom === DIC_PROP.PARAM_FROM[1].value);
let res2 = data.subFlowParams.filter(f => f.paramFrom === DIC_PROP.PARAM_FROM[2].value);
splice.push(...res)
splice.push(...res2)
} else if (DIC_PROP.PARAM_FROM[1].value === row.paramFrom) {
let res = data.subFlowParams.filter(f => f.paramFrom === DIC_PROP.PARAM_FROM[0].value);
let res2 = data.subFlowParams.filter(f => f.paramFrom === DIC_PROP.PARAM_FROM[2].value);
splice.push(...res)
splice.push(...res2)
} else if (DIC_PROP.PARAM_FROM[2].value === row.paramFrom) {
let res = data.subFlowParams.filter(f => f.paramFrom === DIC_PROP.PARAM_FROM[0].value);
let res2 = data.subFlowParams.filter(f => f.paramFrom === DIC_PROP.PARAM_FROM[1].value);
splice.push(...res)
splice.push(...res2)
}
data.subFlowParams = splice
methods.changeSubFlowParams()
},
validateSubFlowData() {
// 兼容老版本
let subFlowParams = props.currSelect.attributes.attrs.cdata.attrs.subFlowParams;
if (!subFlowParams) {
props.currSelect.attributes.attrs.cdata.attrs.subFlowParams = []
}
let startSubMethod = props.currSelect.attributes.attrs.cdata.attrs.startSubMethod;
if (!startSubMethod) props.currSelect.attributes.attrs.cdata.attrs.startSubMethod = 'POST'
let restartSubMethod = props.currSelect.attributes.attrs.cdata.attrs.restartSubMethod;
if (!restartSubMethod) props.currSelect.attributes.attrs.cdata.attrs.restartSubMethod = 'PUT'
let backParMethod = props.currSelect.attributes.attrs.cdata.attrs.backParMethod;
if (!backParMethod) props.currSelect.attributes.attrs.cdata.attrs.backParMethod = 'PUT'
data.subFlowParams = props.currSelect.attributes.attrs.cdata.attrs.subFlowParams
},
changeSubFlowParams() {
props.currSelect.attributes.attrs.cdata.attrs.subFlowParams = data.subFlowParams
},
changeTabPane(val) {
methods.validateSubFlowData()
methods.listFormFieldPerms()
}
}
// 监听双向绑定
watch(
() => props.currSelect,
(val) => {
if (Object.keys(val).length === 0) {
return
}
methods.changeTabPane(val)
}
);
</script>
<style lang="scss">
@import '../assets/style/flow-attr.scss';
</style>

View File

@@ -0,0 +1,666 @@
<template>
<el-form-item label="条件组关系">
<el-switch
v-model="data.cond.groupsType" @change="methods.changeGroupsType"
active-value="0"
inactive-value="1"
inactive-text=""
active-text="" disabled>
</el-switch>
<el-tooltip placement="top">
<template #content>一个组决定一个参与者多条件组满足则累加参与者</template>
<el-icon style="margin-left: 10px">
<QuestionFilled/>
</el-icon>
</el-tooltip>
</el-form-item>
<el-form-item label="审批规则模式">
<el-radio-group style="width: 313px" @change="methods.handleCondValType" :disabled="data.existData"
v-model="data.cond.valType">
<el-radio v-for="(item, index) in [DIC_PROP.VAL_TYPE[2], DIC_PROP.VAL_TYPE[4], DIC_PROP.VAL_TYPE[5]]" :key="index" :label="item.value"
style="width: 75px">
{{ item.label }}
</el-radio>
</el-radio-group>
<el-tooltip placement="top">
<template #content>若无法切换模式请先清空条件组列表</template>
<el-icon>
<QuestionFilled/>
</el-icon>
</el-tooltip>
</el-form-item>
<el-divider>组内条件配置</el-divider>
<template v-if="data.cond.valType === '0'">
<el-form-item label="组内条件关系">
<el-switch
v-model="data.cond.groupType"
active-value="0"
inactive-value="1"
inactive-text=""
active-text="">
</el-switch>
</el-form-item>
<el-table :data="data.cond.condGroup"
border style="width: 100%; margin-bottom: 10px" max-height="500">
<el-table-column type="index" label="操作" width="80">
<template #header>
<el-button icon="Plus" size="small" type="primary" circle @click="methods.onAddItem(true)"></el-button>
</template>
<template #default="scope">
<el-button icon="Minus" size="small" type="danger" circle @click="methods.handleCondDelete(scope.$index, scope.row)"></el-button>
</template>
</el-table-column>
<el-table-column prop="varKeyVal" :label="t('flowClazz.varKeyVal')" show-overflow-tooltip>
<template #default="scope">
<el-select v-model="scope.row.varKeyVal" @change="methods.handleVarKeyVal(scope.row)"
clearable
filterable>
<el-option-group
v-for="(group, index) in data.allFieldPerms"
:key="index"
:label="group.label">
<el-option
v-for="(item, index) in group.options"
:disabled="item.prop.indexOf('_define_') !== -1"
:key="index"
:label="item.label"
:value="item.prop">
</el-option>
</el-option-group>
</el-select>
</template>
</el-table-column>
<el-table-column prop="operator" :label="t('flowClazz.operator')" show-overflow-tooltip>
<template #default="scope">
<el-select v-model="scope.row.operator" @change="methods.handleVarKeyVal(scope.row)"
clearable>
<el-option
v-for="(item, index) in data.varValOperator"
:key="index"
:label="item.label"
:value="item.value">
</el-option>
</el-select>
</template>
</el-table-column>
<el-table-column prop="varVal" :label="t('flowClazz.varVal')" show-overflow-tooltip>
<template #default="scope">
<el-input v-model="scope.row.varVal" clearable/>
</template>
</el-table-column>
</el-table>
<el-form-item label="参与者类型">
<el-select v-model="data.cond.jobType" @change="methods.changeJobType(data.cond)">
<el-option v-for="(item, index) in DIC_PROP.JOB_USER_TYPE" :key="index" :label="item.label" :value="item.value"></el-option>
</el-select>
</el-form-item>
<el-form-item label="哪个人来审批" v-if="data.cond.jobType === DIC_PROP.JOB_USER_TYPE[0].value">
<el-tooltip content="请输入用户名称进行模糊搜索" placement="top">
<el-select class="input-attr"
v-model="data.cond.roleId"
clearable filterable
remote :remote-method="methodsRemote.remoteMethodUser2" :reserve-keyword="false">
<template v-for="(item, index) in dicData.users2" :key="index">
<el-option :label="item.name"
:value="item.userId">
</el-option>
</template>
</el-select>
</el-tooltip>
<el-tooltip placement="top" content="组内条件满足时,由哪个人来审批">
<el-icon style="margin-left: 10px"><QuestionFilled /></el-icon>
</el-tooltip>
</el-form-item>
<el-form-item label="哪个角色审批" v-if="data.cond.jobType === DIC_PROP.JOB_USER_TYPE[1].value">
<el-tooltip content="请输入角色名称进行模糊搜索" placement="top">
<el-select class="input-attr" v-model="data.cond.roleId"
filterable
clearable
remote :remote-method="methodsRemote.remoteMethodRole2" :reserve-keyword="false">
<el-option
v-for="(item, index) in dicData.roles2"
:key="index"
:label="item.roleName"
:value="item.roleId">
</el-option>
</el-select>
</el-tooltip>
<el-tooltip placement="top" content="组内条件满足时,由哪个角色来审批">
<el-icon style="margin-left: 10px"><QuestionFilled /></el-icon>
</el-tooltip>
</el-form-item>
<el-form-item label="哪个岗位审批" v-if="data.cond.jobType === DIC_PROP.JOB_USER_TYPE[2].value">
<el-tooltip content="请输入岗位名称进行模糊搜索" placement="top">
<el-select class="input-attr" v-model="data.cond.roleId"
filterable
clearable
remote :remote-method="methodsRemote.remoteMethodPost2" :reserve-keyword="false">
<el-option
v-for="(item, index) in dicData.posts2"
:key="index"
:label="item.postName"
:value="item.postId">
</el-option>
</el-select>
</el-tooltip>
<el-tooltip placement="top" content="组内条件满足时,由哪个岗位来审批">
<el-icon style="margin-left: 10px"><QuestionFilled /></el-icon>
</el-tooltip>
</el-form-item>
<el-form-item label="哪个部门审批" v-if="data.cond.jobType === DIC_PROP.JOB_USER_TYPE[3].value">
<el-tooltip content="请输入部门名称进行模糊搜索" placement="top">
<el-select class="input-attr" v-model="data.cond.roleId"
filterable
clearable
remote :remote-method="methodsRemote.remoteMethodDept2" :reserve-keyword="false">
<el-option
v-for="(item, index) in dicData.depts2"
:key="index"
:label="item.name"
:value="item.deptId">
</el-option>
</el-select>
</el-tooltip>
<el-tooltip placement="top" content="组内条件满足时,由哪个部门审批">
<el-icon style="margin-left: 10px"><QuestionFilled /></el-icon>
</el-tooltip>
</el-form-item>
</template>
<el-form-item label="函数表达式" v-if="data.cond.valType === '2'">
<el-input class="input-attr" v-model="data.cond.varKeyVal" placeholder="请输入函数表达式" clearable @blur="methods.handleCondVarKeyVal"/>
<el-tooltip placement="top">
<template #content>采用表达式取值 ( 以下两种方式均支持自定义任意扩展 ), 值可以为对象 数组, 满足您分配参与者复杂的场景:
<br />{{ PROP_CONST.TEXT_DESC.condUserExplain }}
<br />{{ PROP_CONST.TEXT_DESC.condMethodExplain3 }}, 返回值可以为对象 数组{{ PROP_CONST.TEXT_DESC.condMethodExplain5 }}
<br />{{ PROP_CONST.TEXT_DESC.condMethodExplain4 }}
</template>
<el-icon style="margin-left: 10px">
<QuestionFilled/>
</el-icon>
</el-tooltip>
</el-form-item>
<div v-if="data.cond.valType === '2'" style="margin: 10px 13px">
<span style="color: #409EFF;font-size: 14px">: 当前函数表达式的返回值可以为对象 数组{{ PROP_CONST.TEXT_DESC.condMethodExplain5 }}</span>
</div>
<template v-if="data.cond.valType === '3'">
<el-form-item label="Http请求地址">
<el-input class="input-attr" placeholder="可输入全路径或相对路径" v-model="data.cond.varKeyVal"
@blur="methods.handleCondVarKeyVal" clearable>
<template #prepend>
<el-select v-model="data.cond.httpMethod">
<el-option v-for="(item, index) in DIC_PROP.HTTP_METHODS" :key="index" :label="item.label" :value="item.value"></el-option>
</el-select>
</template>
</el-input>
<el-tooltip content="点击可设置Http请求的参数信息" placement="bottom">
<el-button type="primary" size="small" round style="margin-left: 10px"
@click="methods.openHttpUrlParams()">
{{ data.httpUrlParamsVisible ? '清空' : '参数' }}
</el-button>
</el-tooltip>
</el-form-item>
<flow-http-param ref="flowHttpParam" :currFlowForm="props.currFlowForm" :flowData="props.flowData"
:httpParam="props.currSelect.attributes.attrs.cdata.defJob"
:httpParamType="DIC_PROP.PARAM_RULE_TYPE[0].value"
v-if="data.httpUrlParamsVisible"></flow-http-param>
</template>
<template v-if="data.cond.valType === '0'">
<el-tooltip content="当节点属性【多人审批方式】为依次审批时,审批顺序由下方条件组的顺序决定" placement="bottom">
<el-button type="primary" round style="margin-left: 185px; margin-top: 30px; margin-bottom: 30px; width: 200px" @click="methods.addFlowNodeCondGroup()">
{{ data.oldCurrentRow ? '修改完成': '添加条件组' }}
</el-button>
</el-tooltip>
<el-divider>已添加条件组列表(点击行可再次修改)</el-divider>
<el-empty description="条件组列表为空" style="margin: 10px 230px" v-if="!data.existData">
</el-empty>
<template v-for="(item, index) in data.condGroups" v-else
:key="index">
<el-collapse v-model="data.collapse">
<el-collapse-item :name="index">
<template #title>
{{ '条件组 ' + (index + 1) }}
<el-icon style="margin-left: 10px" @click="methods.handleCondGroupDelete(index)">
<Delete />
</el-icon>
</template>
<el-form-item label="组内条件关系">
<el-switch
v-model="item.groupType" @change="methods.changeGroupType(item, index)"
active-value="0"
inactive-value="1"
inactive-text=""
active-text="">
</el-switch>
</el-form-item>
<el-table :data="item.condGroup" border style="width: 100%; margin-bottom: 10px" max-height="500"
highlight-current-row @current-change="methods.handleCurrentChange" :ref="'tableDataRef' + index">
<el-table-column type="index" label="操作" width="80">
<template #default="scope">
<el-button icon="Minus" size="small" type="danger" circle
@click="methods.handleSingleCondDelete(scope.$index, scope.row, index)"></el-button>
</template>
</el-table-column>
<el-table-column prop="varKeyVal" :label="t('flowClazz.varKeyVal')" show-overflow-tooltip>
<template #default="scope">
<convert-group-name :options="data.allFieldPerms"
:value="scope.row.varKeyVal"
:valueKey="'prop'" :showKey="'label'"></convert-group-name>
</template>
</el-table-column>
<el-table-column prop="operator" :label="t('flowClazz.operator')" show-overflow-tooltip>
<template #default="scope">
<dict-tag :options="DIC_PROP.OPERATOR" :value="scope.row.operator"></dict-tag>
</template>
</el-table-column>
<el-table-column prop="varVal" :label="t('flowClazz.varVal')" show-overflow-tooltip/>
</el-table>
<el-form-item label="参与者类型">
<el-select v-model="item.jobType" @change="methods.changeJobType(item)">
<el-option v-for="(item, index) in DIC_PROP.JOB_USER_TYPE" :key="index" :label="item.label" :value="item.value"></el-option>
</el-select>
</el-form-item>
<el-form-item label="哪个人来审批" v-if="item.jobType === DIC_PROP.JOB_USER_TYPE[0].value">
<el-tooltip content="请输入用户名称进行模糊搜索" placement="top">
<el-select class="input-attr"
v-model="item.roleId"
clearable filterable
remote :remote-method="methodsRemote.remoteMethodUser" :reserve-keyword="false">
<template v-for="(item, index) in dicData.users" :key="index">
<el-option :label="item.name"
:value="item.userId">
</el-option>
</template>
</el-select>
</el-tooltip>
<el-tooltip placement="top" content="组内条件满足时,由哪个人来审批">
<el-icon style="margin-left: 10px"><QuestionFilled /></el-icon>
</el-tooltip>
</el-form-item>
<el-form-item label="哪个角色审批" v-if="item.jobType === DIC_PROP.JOB_USER_TYPE[1].value">
<el-tooltip content="请输入角色名称进行模糊搜索" placement="top">
<el-select class="input-attr" v-model="item.roleId"
filterable
clearable
remote :remote-method="methodsRemote.remoteMethodRole" :reserve-keyword="false">
<el-option
v-for="(item, index) in dicData.roles"
:key="index"
:label="item.roleName"
:value="item.roleId">
</el-option>
</el-select>
</el-tooltip>
<el-tooltip placement="top" content="组内条件满足时,由哪个角色来审批">
<el-icon style="margin-left: 10px"><QuestionFilled /></el-icon>
</el-tooltip>
</el-form-item>
<el-form-item label="哪个岗位审批" v-if="item.jobType === DIC_PROP.JOB_USER_TYPE[2].value">
<el-tooltip content="请输入岗位名称进行模糊搜索" placement="top">
<el-select class="input-attr" v-model="item.roleId"
filterable
clearable
remote :remote-method="methodsRemote.remoteMethodPost" :reserve-keyword="false">
<el-option
v-for="(item, index) in dicData.posts"
:key="index"
:label="item.postName"
:value="item.postId">
</el-option>
</el-select>
</el-tooltip>
<el-tooltip placement="top" content="组内条件满足时,由哪个岗位来审批">
<el-icon style="margin-left: 10px"><QuestionFilled /></el-icon>
</el-tooltip>
</el-form-item>
<el-form-item label="哪个部门审批" v-if="item.jobType === DIC_PROP.JOB_USER_TYPE[3].value">
<el-tooltip content="请输入部门名称进行模糊搜索" placement="top">
<el-select class="input-attr" v-model="item.roleId"
filterable
clearable
remote :remote-method="methodsRemote.remoteMethodDept" :reserve-keyword="false">
<el-option
v-for="(item, index) in dicData.depts"
:key="index"
:label="item.name"
:value="item.deptId">
</el-option>
</el-select>
</el-tooltip>
<el-tooltip placement="top" content="组内条件满足时,由哪个部门审批">
<el-icon style="margin-left: 10px"><QuestionFilled /></el-icon>
</el-tooltip>
</el-form-item>
</el-collapse-item>
</el-collapse>
</template>
</template>
</template>
<script setup lang="ts" name="FlowUserRule">
import {useI18n} from "vue-i18n";
import {useMessage} from "/@/hooks/message";
import {rule, validateNull} from "/@/utils/validate";
import {deepClone} from "/@/utils/other";
import {DIC_PROP} from "/@/flow/support/dict-prop";
import {buildSysFieldsFormOption} from "../../utils/form-perm";
import {onFormLoadedUrl, onLoadDicUrl, initRemoteMethodAllByKey} from "../../components/convert-name/convert";
import {PROP_CONST} from "../../support/prop-const";
import {handleChangeJobType} from "../../index";
const {proxy} = getCurrentInstance();
const FlowHttpParam = defineAsyncComponent(() => import('./flow-http-param.vue'));
const {t} = useI18n();
const $message = useMessage();
const props = defineProps({
currFlowForm: {
type: Object,
default: null,
},
currSelect: {
type: Object,
default: null,
},
flowData: {
type: Object,
default: null,
},
});
const condData = {
httpMethod: 'GET',
roleId: null,
jobType: null,
groupsType: '1',
condGroup: [],
groupType: '0',
valType: '0',
varKeyVal: null,
operator: null,
varVal: null,
}
const data = reactive({
collapse: [0],
allFieldPerms: [],
formFieldPerms: [],
cond: deepClone(condData),
httpUrlParamsVisible: false,
oldCurrentRow: null,
condGroups: [],
existData: false
})
// 定义字典
const dicData = reactive({});
const onLoad = onLoadDicUrl();
const onFormLoaded = onFormLoadedUrl(...PROP_CONST.LOAD_USER_ROLE);
onMounted(async () => {
// await onLoad(dicData);
methods.changeTabPane(props.currSelect)
})
const methodsRemote = initRemoteMethodAllByKey(onLoad, dicData)
const methods = {
changeJobType(item) {
handleChangeJobType(dicData, item)
},
onFormLoaded() {
let condGroups = props.currSelect.attributes.attrs.cdata.defJob.condGroups
if (validateNull(condGroups)) return
onFormLoaded(dicData, condGroups)
},
openHttpUrlParams() {
if (!data.cond.varKeyVal) {
$message.warning("请先输入【Http请求地址】")
return
}
if (data.httpUrlParamsVisible) {
props.currSelect.attributes.attrs.cdata.defJob.httpParams = []
data.httpUrlParamsVisible = false
} else {
data.httpUrlParamsVisible = true
}
},
handleCurrentChange(row) {
if (!row) return
let condGroupsRow;
for (let i = 0; i < data.condGroups.length; i++) {
let index = data.condGroups[i].condGroup.indexOf(row);
if (index !== -1) {
condGroupsRow = data.condGroups[i];
}
}
// 先清空之前选中的其他条件组
if (data.oldCurrentRow !== condGroupsRow) {
let oldIndex = data.condGroups.indexOf(data.oldCurrentRow);
if (oldIndex !== -1) {
proxy.$refs['tableDataRef' + oldIndex][0].setCurrentRow(null)
}
}
data.cond = condGroupsRow
data.oldCurrentRow = condGroupsRow
},
handleVarKeyVal(row) {
let dots = row.varKeyVal.split('.').length - 1;
if (dots === 2) {
let find = DIC_PROP.OPERATOR.slice(6).find(f => f.value === row.operator);
if (!find && row.operator) {
$message.warning("子表单的字段只能选择包含或不包含")
row.operator = null
}
}
},
changeGroupsType(groupsType) {
let condGroups = props.currSelect.attributes.attrs.cdata.defJob.condGroups;
if (validateNull(condGroups)) return
props.currSelect.attributes.attrs.cdata.defJob.condGroups.forEach(each => {
each.groupsType = groupsType
})
methods.validateCondData()
},
changeGroupType(item, index) {
let condGroups = props.currSelect.attributes.attrs.cdata.defJob.condGroups;
if (validateNull(condGroups)) return
props.currSelect.attributes.attrs.cdata.defJob.condGroups[index].groupType = item.groupType
methods.validateCondData()
},
onAddItem(isAdd) {
if (validateNull(data.formFieldPerms)) {
buildSysFieldsFormOption(data, props, $message)
}
if (data.cond.condGroup.length > 0) {
let find = data.cond.condGroup.find(f => !f.varKeyVal || !f.operator || !f.varVal);
if (find) {
let b = !find.varKeyVal || !find.operator || !find.varVal;
if (isAdd && b) {
$message.warning("请先填写 表单字段 或 运算符 或 值")
return
}
}
if (!isAdd) data.cond.condGroup.splice(0, data.cond.condGroup.length);
}
let obj = {varKeyVal: '', operator: '', varVal: ''};
data.cond.condGroup.push(obj);
},
addFlowNodeCondGroup() {
if (validateNull(data.cond.condGroup)) {
$message.warning("请先添加组内条件")
return
}
let valType = data.cond.valType;
let find = data.cond.condGroup.find(f => !f.varKeyVal || !f.operator || !f.varVal);
if (find) {
if (valType === DIC_PROP.VAL_TYPE[2].value) {
if (!find.varKeyVal || !find.operator || !find.varVal) {
$message.warning("表单字段 或 运算符 或 值 不能为空")
return
}
}
}
if (!data.cond.roleId) {
$message.warning("参与者 不能为空")
return
}
if (data.oldCurrentRow) {
// 先清空之前选中
let index = data.condGroups.indexOf(data.cond);
if (index !== -1) {
proxy.$refs['tableDataRef' + index][0].setCurrentRow(null)
}
data.oldCurrentRow = null
data.cond = deepClone(condData);
} else {
let cond = deepClone(data.cond);
props.currSelect.attributes.attrs.cdata.defJob.condGroups.push(cond)
methods.validateCondData()
methods.updateCondVarKeyVal(true)
}
methods.onFormLoaded()
},
handleCondGroupDelete(index: number) {
props.currSelect.attributes.attrs.cdata.defJob.condGroups.splice(index, 1)
methods.validateCondData()
},
handleSingleCondDelete(index: number, row: any, groupIndex) {
props.currSelect.attributes.attrs.cdata.defJob.condGroups[groupIndex].condGroup.splice(index, 1)
if (validateNull(props.currSelect.attributes.attrs.cdata.defJob.condGroups[groupIndex].condGroup)) {
methods.handleCondGroupDelete(groupIndex)
}
methods.validateCondData()
},
handleCondDelete(index: number, row: any) {
data.cond.condGroup.splice(index, 1)
},
handleCondValType(type?) {
if (type) {
data.cond.varKeyVal = null
methods.updateCondVarKeyVal(false)
}
if (!type) {
type = methods.initCurrVarKeyVal()
}
if (type === DIC_PROP.VAL_TYPE[2].value) {
data.varValOperator = DIC_PROP.OPERATOR
methods.onAddItem(false)
} else {
data.varValOperator = []
}
data.cond.operator = null
data.cond.varVal = null
},
handleCondVarKeyVal() {
let val = data.cond.varKeyVal
let valType = data.cond.valType;
if (!val) {
methods.updateCondVarKeyVal(false)
return
}
if (valType === DIC_PROP.VAL_TYPE[2].value) return
if (valType === DIC_PROP.VAL_TYPE[4].value && val.indexOf("#") === -1) {
data.cond.varKeyVal = null
$message.warning("当选择专业模式时, 函数表达式必须符合规定的格式")
return;
}
methods.updateCondVarKeyVal(true)
},
updateCondVarKeyVal(isSave) {
props.currSelect.attributes.attrs.cdata.defJob.valType = data.cond.valType
if (isSave) {
if (data.cond.valType === DIC_PROP.VAL_TYPE[2].value) {
props.currSelect.attributes.attrs.cdata.defJob.userKeyVal = PROP_CONST.VAR_KEY_VAL.person
props.currSelect.attributes.attrs.cdata.defJob.userKeyValName = PROP_CONST.VAR_KEY_VAL.personName
} else {
props.currSelect.attributes.attrs.cdata.defJob.userKeyVal = data.cond.varKeyVal
}
props.currSelect.attributes.attrs.cdata.defJob.httpMethod = data.cond.httpMethod
} else {
props.currSelect.attributes.attrs.cdata.defJob.userKeyVal = null
props.currSelect.attributes.attrs.cdata.defJob.userKeyValName = null
props.currSelect.attributes.attrs.cdata.defJob.condGroups = []
props.currSelect.attributes.attrs.cdata.defJob.httpParams = []
props.currSelect.attributes.attrs.cdata.defJob.httpMethod = null
data.httpUrlParamsVisible = false
}
},
validateCondData() {
let condGroups = props.currSelect.attributes.attrs.cdata.defJob.condGroups;
if (!condGroups) {
props.currSelect.attributes.attrs.cdata.defJob.condGroups = []
}
data.condGroups.splice(0, data.condGroups.length);
props.currSelect.attributes.attrs.cdata.defJob.condGroups.forEach(each => data.condGroups.push(each))
data.existData = !validateNull(data.condGroups)
},
initCurrVarKeyVal() {
data.cond.valType = props.currSelect.attributes.attrs.cdata.defJob.valType
if (data.condGroups.length <= 0) {
data.cond.varKeyVal = props.currSelect.attributes.attrs.cdata.defJob.userKeyVal
let httpMethod = props.currSelect.attributes.attrs.cdata.defJob.httpMethod
if (httpMethod) data.cond.httpMethod = httpMethod
}
let httpParams = props.currSelect.attributes.attrs.cdata.defJob.httpParams;
if (!validateNull(httpParams)) {
data.httpUrlParamsVisible = true
}
return data.cond.valType
},
changeTabPane(val) {
methods.validateCondData()
methods.handleCondValType()
methods.onFormLoaded()
}
}
// 监听双向绑定
watch(
() => props.currSelect,
(val) => {
if (Object.keys(val).length === 0) {
return
}
methods.changeTabPane(val)
}
);
</script>
<style lang="scss">
@import '../assets/style/flow-attr.scss';
</style>

View File

@@ -0,0 +1,252 @@
import {DIC_PROP} from "/@/flow/support/dict-prop";
import {PROP_CONST} from "/@/flow/support/prop-const";
import {CommonNodeType, HighNodeType} from "/@/flow/designer/config/type";
import {notifyLeft} from "/@/flow";
import {useMessageBox} from "/@/hooks/message";
import {validateNull} from "/@/utils/validate";
import {gateAttr, linkAttr, syncJobAttr, syncNodeAttr} from "/@/flow/designer/config/attr-config";
import {setPropsNullValue} from "/@/flow/support/common";
import {validateListFormOption} from "/@/flow/utils/form-perm";
/**
* 常用工具类
*
* @author luolin
*/
// 反解析名称时未加载该常量
export function revParseWhoseLeaderName(data, dicData) {
// 谁的主管,可自定义更多
dicData.users.unshift(PROP_CONST.FLOW_METHOD.whoseLeader)
if (!data.whoseLeader) return
if (data.whoseLeader === PROP_CONST.FLOW_METHOD.whoseLeader.userId) {
data.whoseLeaderName = PROP_CONST.FLOW_METHOD.whoseLeader.name
} else {
let find = dicData.users.find(f => f.userId === data.whoseLeader);
if (find) data.whoseLeaderName = find.name;
}
}
export async function revParseUserKeyValName(props, data, dicData, methods) {
if (methods.validateCurrSelectDefJob) {
if (!methods.validateCurrSelectDefJob()) return;
}
let valType = props.currSelect.attributes.attrs.cdata.defJob.valType;
let userKeyVal = props.currSelect.attributes.attrs.cdata.defJob.userKeyVal;
if (!userKeyVal) return;
let userKeyValFrom;
if (userKeyVal === PROP_CONST.VAR_KEY_VAL.order + 'createUser') {
userKeyValFrom = DIC_PROP.FLOW_METHOD_TYPE[0].value
data.activeKey = 'flow-method'
} else if (userKeyVal.indexOf('getUserDeptLeaderId') !== -1) {
userKeyValFrom = DIC_PROP.FLOW_METHOD_TYPE[1].value
data.whoseLeader = userKeyVal.substring(userKeyVal.indexOf('(Long#') + 6, userKeyVal.indexOf(',Integer#'))
data.leaderLevel = parseInt(userKeyVal.substring(userKeyVal.indexOf(',Integer#') + 9, userKeyVal.indexOf(',String#')))
data.levelExtract = userKeyVal.substring(userKeyVal.indexOf(',String#') + 8, userKeyVal.indexOf(')'))
data.activeKey = 'flow-method'
} else if (userKeyVal.indexOf('listUserDeptMultiLeaderId') !== -1) {
userKeyValFrom = DIC_PROP.FLOW_METHOD_TYPE[2].value
let lastIndex = userKeyVal.indexOf('String#') - 1;
data.whoseLeader = userKeyVal.substring(userKeyVal.indexOf('(Long#') + 6, lastIndex)
userKeyVal = userKeyVal.substr(lastIndex)
lastIndex = userKeyVal.indexOf('Integer#') - 1;
data.auditEndpoint = userKeyVal.substring(userKeyVal.indexOf('String#') + 7, lastIndex)
userKeyVal = userKeyVal.substr(lastIndex)
lastIndex = userKeyVal.indexOf('String#') - 1;
let leaderLevel = userKeyVal.substring(userKeyVal.indexOf('Integer#') + 8, lastIndex)
userKeyVal = userKeyVal.substr(lastIndex)
if (data.auditEndpoint === '1') data.leaderLevel = parseInt(leaderLevel)
lastIndex = userKeyVal.lastIndexOf('String#') - 1;
data.seqAuditSort = userKeyVal.substring(userKeyVal.indexOf('String#') + 7, lastIndex)
userKeyVal = userKeyVal.substr(lastIndex)
data.levelExtract = userKeyVal.substring(userKeyVal.indexOf(')') - 1, userKeyVal.indexOf(')'))
data.activeKey = 'flow-method'
} else if (userKeyVal.indexOf('getDeptLeaderId') !== -1) {
userKeyValFrom = DIC_PROP.FLOW_METHOD_TYPE[3].value
data.appointDeptId = userKeyVal.substring(userKeyVal.indexOf('(Long#') + 6, userKeyVal.indexOf(',String#'))
data.levelExtract = userKeyVal.substring(userKeyVal.indexOf(',String#') + 8, userKeyVal.indexOf(')'))
} else if (props.currSelect.attributes.attrs.cdata.defJob.userKeyValName) {
let find = DIC_PROP.FLOW_METHOD_TYPE.find(f => f.label === props.currSelect.attributes.attrs.cdata.defJob.userKeyValName);
// 再次编辑时优先显示为专业模式
if (find) {
userKeyValFrom = DIC_PROP.FLOW_METHOD_TYPE[4].value
data.userKeyVal = userKeyVal
if (methods.handleUserKeyValFrom) methods.handleUserKeyValFrom(userKeyValFrom)
data.activeKey = 'flow-method'
}
} else if (DIC_PROP.VAL_TYPE[4].value === valType && userKeyVal) {
if (validateNull(data.formFieldPerms)) await validateListFormOption(data, props)
if (!validateNull(data.formFieldPerms)) {
let exist = data.formFieldPerms.find(f => f.prop === userKeyVal);
if (exist) userKeyValFrom = DIC_PROP.FLOW_METHOD_TYPE[4].value
}
}
if (userKeyValFrom) {
data.userKeyValFrom = userKeyValFrom
} else {
data.activeKey = 'flow-rule'
}
}
export function parseUserKeyValName(props, data, methods) {
if (methods.validateCurrSelectDefJob) {
if (!methods.validateCurrSelectDefJob()) return;
}
let userKeyVal;
if (data.userKeyValFrom === '0') {
userKeyVal = PROP_CONST.VAR_KEY_VAL.order + 'createUser'
} else if (data.userKeyValFrom === '1') {
userKeyVal = '#distActorServiceImpl.getUserDeptLeaderId(Long#'+ data.whoseLeader +',Integer#'+ data.leaderLevel +',String#'+ data.levelExtract +')'
} else if (data.userKeyValFrom === '2') {
let leaderLevel = 'NULL'
if (data.auditEndpoint === '1') leaderLevel = data.leaderLevel
let seqAuditSort = 'NULL'
if (data.seqAuditSort) seqAuditSort = data.seqAuditSort
userKeyVal = '#distActorServiceImpl.listUserDeptMultiLeaderId(Long#'+ data.whoseLeader +',String#'+ data.auditEndpoint +',Integer#'+ leaderLevel +',String#'+ seqAuditSort +',String#'+ data.levelExtract +')'
} else if (data.userKeyValFrom === '3') {
userKeyVal = '#distActorServiceImpl.getDeptLeaderId(Long#'+ data.appointDeptId +',String#'+ data.levelExtract +')'
} else if (data.userKeyValFrom === '4') {
userKeyVal = data.userKeyVal
}
if (data.userKeyValFrom === '1' || data.userKeyValFrom === '2') {
if (!data.whoseLeader) {
if (methods.$message) methods.$message('whoseLeader')
return
}
}
if (data.userKeyValFrom === '3') {
if (!data.appointDeptId) {
if (methods.$message) methods.$message('appointDeptId')
return
}
}
props.currSelect.attributes.attrs.cdata.defJob.userKeyVal = userKeyVal;
props.currSelect.attributes.attrs.cdata.defJob.userKeyValName = DIC_PROP.FLOW_METHOD_TYPE.find(f => f.value === data.userKeyValFrom).label;
props.currSelect.attributes.attrs.cdata.defJob.valType = DIC_PROP.VAL_TYPE[4].value
// 清空其他参数
props.currSelect.attributes.attrs.cdata.defJob.condGroups = []
props.currSelect.attributes.attrs.cdata.defJob.httpParams = []
props.currSelect.attributes.attrs.cdata.defJob.httpMethod = null
}
export function handleLinkFlowNodeIds(data, props) {
data.toFlowNodeIds = []
data.fromFlowNodeIds = []
let models = window._jfGraph.getElements();
data.fromFlowNodeId = props.currSelect.attributes.source.id
data.toFlowNodeId = props.currSelect.attributes.target.id
// 修正拖拽连线箭头更改目标节点
props.currSelect.attributes.attrs.cdata.attrs.fromFlowNodeId = data.fromFlowNodeId
props.currSelect.attributes.attrs.cdata.attrs.toFlowNodeId = data.toFlowNodeId
models.forEach(each => {
if (!validateNodeType(each)) return
let id = each.id
let nodeName = each.attributes.attrs.label.text
if (id !== props.currSelect.attributes.target.id) {
data.fromFlowNodeIds.push({id, nodeName})
}
if (id !== props.currSelect.attributes.source.id) {
data.toFlowNodeIds.push({id, nodeName})
}
})
}
export function changeLinkFlowNodeIds(data, props, methods?, $emit?) {
useMessageBox()
.confirm('是否确认修改连线的' + (data.modifyPointType === '0' ? '起点?' : '终点?'))
.then(() => {
doLinkFlowNodeIds(data, props, methods, $emit)
})
}
function doLinkFlowNodeIds(data, props, methods?, $emit?) {
if (data.modifyPointType === '0') {
props.currSelect.attributes.attrs.cdata.attrs.fromFlowNodeId = data.fromFlowNodeId
props.currSelect.set('source', { id: data.fromFlowNodeId });
} else {
props.currSelect.attributes.attrs.cdata.attrs.toFlowNodeId = data.toFlowNodeId
props.currSelect.set('target', { id: data.toFlowNodeId });
}
if (methods) methods.handleLinkFlowNodeIds()
if (window._flowConfig.globalConfig.isSimpleMode === '1') window._jfOperate.layout()
else notifyLeft('专业模式不会自动调整连线轨迹,有必要时请手动调整', 'warning', 3000)
if ($emit) $emit("hideAttrConfig", false, '1');
}
export function validateNodeType(currSelect, methods?, isVirtual?) {
if (methods && !methods.validateCurrSelectAttrs()) return false;
let type = currSelect.attributes.attrs.cdata.type;
let noVirtual = type === CommonNodeType.START || type === CommonNodeType.END || type === CommonNodeType.SERIAL || type === CommonNodeType.PARALLEL;
if (!isVirtual) return noVirtual
return noVirtual || type === HighNodeType.VIRTUAL
}
export function handleSyncFlowNodeIds(data, props, methods) {
data.syncFlowNodeIds = []
if (!methods.validateCurrSelectAttrsAttrs()) return;
props.currSelect.attributes.attrs.cdata.attrs.syncFlowNodeId = null
let isGateway = props.currSelect.attributes.attrs.cdata.attrs.isGateway;
let models = window._jfGraph.getElements();
if (validateNull(models)) return
models.forEach(each => {
let cdata = each.attributes.attrs.cdata;
let b = cdata.type === CommonNodeType.SERIAL || cdata.type === CommonNodeType.PARALLEL;
if (b && props.currSelect.id !== each.id) {
let id = each.id
let nodeName = each.attributes.attrs.label.text + "ID:" + id + ""
let isExist = false
if (isGateway === '1') {
if (cdata.attrs.isGateway === '1') isExist = true
} else {
if (cdata.attrs.isGateway !== '1') isExist = true
}
if (isExist) data.syncFlowNodeIds.push({id, nodeName})
}
})
}
export function changeSyncFlowNodeId(id, props, methods, $message) {
let models = window._jfGraph.getElements();
if (validateNull(models) || !id) return
let isGateway = props.currSelect.attributes.attrs.cdata.attrs.isGateway;
let cdata = models.find(f => f.id === id).attributes.attrs.cdata;
let nodeAttrs: any[];
if (isGateway !== '1') {
nodeAttrs = Object.keys(syncNodeAttr);
if (!validateNull(cdata.defJob)) {
setPropsNullValue(props.currSelect.attributes.attrs.cdata.defJob, cdata.defJob, ...Object.keys(syncJobAttr))
}
} else {
nodeAttrs = Object.keys(gateAttr);
}
setPropsNullValue(props.currSelect.attributes.attrs.cdata.attrs, cdata.attrs, ...nodeAttrs)
$message.warning("已同步其他节点的配置,请重新打开查看")
methods.hideAttrConfig(false, '1');
}
export function handleSyncFlowNodeRelIds(data, props, methods) {
data.syncFlowNodeRelIds = []
if (!methods.validateCurrSelectAttrsAttrs()) return;
props.currSelect.attributes.attrs.cdata.attrs.syncFlowNodeRelId = null
let links = window._jfGraph.getLinks();
if (validateNull(links)) return
links.forEach(each => {
if (props.currSelect.id !== each.id) {
let id = each.id
let linkName = "ID:" + id
let text = each.attributes.labels[0].attrs.text.text;
if (text) linkName = text + "ID:" + id + ""
data.syncFlowNodeRelIds.push({id, linkName})
}
})
}
export function changeSyncFlowNodeRelId(id, props, methods, $message) {
let links = window._jfGraph.getLinks();
if (validateNull(links) || !id) return
let cdata = links.find(f => f.id === id).attributes.attrs.cdata;
let linkAttrs = Object.keys(linkAttr);
setPropsNullValue(props.currSelect.attributes.attrs.cdata.attrs, cdata.attrs, ...linkAttrs)
$message.warning("已同步其他连线的配置,请重新打开查看")
methods.hideAttrConfig(false, '1');
}

View File

@@ -0,0 +1,87 @@
<template>
<div class="flow-node-menu">
<el-menu style="border-right: 0;">
<template v-for="(node, index) in props.menuList" :key="index">
<el-menu-item style="padding-top: 4px; padding-left: 14px;" v-if="isShowNode(node) && node.type !== HighNodeType.CHILD_FLOW">
<el-tooltip :content="getNodeName(node)" placement="right">
<div
class="el-node-item"
draggable="true"
@dragstart="dragNode(node.type, props.type)"
>
<img :src="node.icon" alt="node">
</div>
</el-tooltip>
</el-menu-item>
</template>
</el-menu>
</div>
</template>
<script setup lang="ts" name="NodeMenu">
import {HighNodeType} from '../config/type'
import {validateNull} from "/@/utils/validate";
const $emit = defineEmits(["setDragInfo"]);
const props = defineProps({
menuList: {
type: Array,
default: () => []
},
flowData: {
type: Object,
default: {}
},
type: {
type: String,
default: null
}
});
function isShowNode(node) {
if (validateNull(props.flowData)) return true
if (node.type !== HighNodeType.JOB && node.type !== HighNodeType.VIRTUAL) {
return true
}
let isJobSeparated = props.flowData.attrs.isJobSeparated;
return isJobSeparated === '1';
}
function getNodeName(node) {
return node.nodeName + (node.nodeDesc ? node.nodeDesc : '')
}
// 开始拖拽
function dragNode(type, belongTo) {
$emit("setDragInfo", {
type,
belongTo
});
}
</script>
<style scoped lang="scss">
/*菜单间距*/
.flow-node-menu {
.el-menu-item {
padding: 11px!important;
height: 40px!important;
}
.el-node-item {
height: 32px;
width: 32px;
color: #fff;
border-radius: 5px;
line-height: 30px;
text-align: center;
cursor: move;
align-items: center;
justify-content: center;
&:hover {
color: #0960bd;
outline: 1px dashed #0960bd;
}
}
}
</style>

View File

@@ -0,0 +1,127 @@
import {deepClone} from "/@/utils/other";
const clazzAttr = {
clazzes: [],
}
const fieldsAttr = {
formFieldPerms: [],
formId: null,
}
const nodeJobLinkAttr = {
condGroups: [],
httpMethod: null,
httpParams: [],
}
const subFlowAttr = {
subFlowParams: [],
startSubFlow: null,
restartSubFlow: null,
backParFlow: null,
startSubMethod: null,
restartSubMethod: null,
backParMethod: null,
}
const flowNodeAttr = {
...deepClone(clazzAttr),
...deepClone(nodeJobLinkAttr),
...deepClone(fieldsAttr),
...deepClone(subFlowAttr),
}
const commonAttr = {
jobBtns: null,
carbonCopy: null,
approveMethod: '1',
ticketCompRate: 100,
routeKeyVal: null,
valType: null,
...deepClone(flowNodeAttr),
pcTodoUrl: null,
subDefFlowId: null,
subFlowVersion: '0',
timeout: 0,
sort: 1,
}
export const startAttr = {
...deepClone(commonAttr),
rejectType: '0'
}
export const syncNodeAttr = {
...deepClone(commonAttr),
nodeApproveMethod: '1',
isAutoNext: '1',
rejectType: '0',
isContinue: '0',
isAutoAudit: '0',
isPassSame: '0'
}
export const nodeAttr = {
description: '',
...deepClone(syncNodeAttr),
}
export const gateAttr = {
...deepClone(flowNodeAttr),
nodeApproveMethod: '1',
isAutoNext: '1',
isAutoAudit: '1',
isGateway: '1',
}
const baseJobAttr = {
...deepClone(nodeJobLinkAttr),
valType: '-2',
userKey: null,
userKeyVal: null,
isNowRun: '0',
timeout: 0,
isSkipRejected: '0',
belongType: null,
sort: 1,
}
export const jobAttr = {
jobName: '节点任务',
...deepClone(baseJobAttr),
distFlowNodeId: null,
}
// currRunJobs
export const syncJobAttr = {
...deepClone(baseJobAttr),
userKeyValName: null,
roleUserId: [],
}
export const endAttr = {
...deepClone(commonAttr),
isAutoAudit: '1',
}
export const virtualAttr = {
...deepClone(commonAttr),
rejectType: '1'
}
export const highAttr = {
childFlowKey: null,
childOrderId: null
}
export const linkAttr = {
...deepClone(nodeJobLinkAttr),
valType: null,
varKeyVal: null,
}
export const laneAttr = {
id: null,
nodeName: null,
}

View File

@@ -0,0 +1,354 @@
import {nodeJobSvgIcons as nodeJobPath} from "/@/flow/designer/assets/svges/path";
const clazzAttr = {
clazzes: [],
}
export const flowAttr = {
id: null,
...clazzAttr,
isIndependent: '0',
allowJobLink: '0',
isJobSeparated: '0',
isSimpleMode: '1',
connector: null,
router: null,
queryOrder: null,
updateOrder: null,
queryMethod: null,
updateMethod: null,
orderParams: [],
flowName: null,
flowKey: null,
groupName: null,
formId: null,
fromType: '1',
version: 1,
status: '-1',
remark: null,
isNew: true,
sort: 1
}
export let flowConfig = {
// ID类型
// 1.uuid uuid 2.time_stamp 时间戳 3.sequence 序列 4.time_stamp_and_sequence 时间戳加序列 5.custom 自定义
idType: ['time_stamp_and_sequence', 6],
flowData: {
// null时init
graph: null,
nodeList: [],
linkList: [],
attrs: flowAttr,
},
mobileConfig: {
mobilePrefix: "appdata",
mobileCode: "jf320920",
},
globalConfig: {
isHideShortcut: '0',
allowJobLink: '0',
isJobSeparated: '0',
isSimpleMode: '1',
connectors: [{value: "normal", label: "简单"}, {value: "rounded", label: "圆角"}, {
value: "jumpover",
label: "跳线"
}, {value: "smooth", label: "平滑"}],
routers: [{value: "normal", label: "简单"}, {value: "manhattan", label: "智能正交"}, {
value: "metro",
label: "智能地铁线"
}, {value: "orthogonal", label: "垂直直角"}, {value: "oneSide", label: "受限正交"}],
rankDirs: [{value: "TB", label: "上下"}, {value: "BT", label: "下上"}, {value: "LR", label: "左右"}, {
value: "RL",
label: "右左"
}],
defaultAutoLayoutName: "TB",
defaultConnectorName: "rounded",
defaultRouterName: "normal",
maxNodeJobNum: 50
},
gridConfig: {
showGrid: false,
showGridText: "显示网格",
showGridIcon: "SwitchButton",
gridSize: 1
},
defaultStyle: {
containerScale: {
init: 1
}
},
viewShortcut: {
paper: {
codeName: '鼠标滚动或点击画布拖拽或小地图',
shortcutName: '拖动画布'
},
flow: {
codeName: '鼠标点击画布隐藏节点信息',
shortcutName: '隐藏节点信息'
},
node: {
codeName: '鼠标放节点上即可查看',
shortcutName: '查看节点信息'
},
link: {
codeName: '鼠标放连线上即可查看',
shortcutName: '查看连线信息'
},
zoomInTool: {
codeName: '画布右上角',
shortcutName: '放大工具'
},
zoomOutTool: {
codeName: '画布右上角',
shortcutName: '缩小工具'
}
},
shortcut: {
note: {
codeName: '图中节点、连线均可拖拽(专业模式),缩放大小',
shortcutName: '简约但不简单'
},
flow: {
codeName: '左上角操作栏',
shortcutName: '切换简单模式与专业模式'
},
flow2: {
codeName: '左上角操作栏',
shortcutName: '设置流程属性'
},
link: {
codeName: '左上角操作栏',
shortcutName: '设置连线、路由、布局'
},
paper: {
codeName: '鼠标滚动或点击画布拖拽或小地图',
shortcutName: '拖动画布'
},
node: {
codeName: '从左侧节点菜单栏拖拽节点(专业模式)',
shortcutName: '新增加点'
},
node1: {
codeName: '鼠标点击节点拖动右下角灰色小圈',
shortcutName: '缩放节点大小'
},
node2: {
codeName: '鼠标放节点边缘显示(+)开始拖出连线',
shortcutName: '节点连线'
},
node3: {
codeName: '鼠标放节点文本上开始拖动节点',
shortcutName: '拖拽节点'
},
node4: {
codeName: '鼠标放节点上双击或右键菜单(专业模式)设置',
shortcutName: '设置节点属性'
},
node41: {
codeName: '鼠标放节点的任务项上单击(节点任务不分离时)设置',
shortcutName: '设置任务属性'
},
node5: {
codeName: '鼠标放节点上右键菜单删除(专业模式)或点击再点X删除',
shortcutName: '删除节点'
},
link2: {
codeName: '鼠标放连线上点击增加拖拽点拖动(专业模式)',
shortcutName: '拖拽连线'
},
link3: {
codeName: '鼠标放连线拖拽点上再次点击删除(专业模式)',
shortcutName: '删除拖拽点'
},
link4: {
codeName: '鼠标放连线上双击(简单模式)或右键菜单(专业模式)设置',
shortcutName: '设置连线属性'
},
link5: {
codeName: '鼠标放连线上点X删除或右键菜单(专业模式)删除',
shortcutName: '删除连线'
},
zoomInTool: {
codeName: '画布右上角',
shortcutName: '放大工具'
},
zoomOutTool: {
codeName: '画布右上角',
shortcutName: '缩小工具'
}
},
contextMenu: {
container: {
menuName: 'flow-menu',
axis: {
x: null,
y: null
},
menulists: [
{
fnHandler: 'flowInfo',
icoName: 'iconfont icon-xianshimima',
btnName: '流程图信息'
},
{
fnHandler: 'paste',
icoName: 'iconfont icon-fuzhiyemian',
btnName: '粘贴'
}
]
},
node: {
menuName: 'node-menu',
axis: {
x: null,
y: null
},
menulists: [
{
fnHandler: 'setNodeAttr',
icoName: 'iconfont icon-quanjushezhi_o',
btnName: '设置属性'
},
{
fnHandler: 'setConnectNode',
icoName: 'iconfont icon-shuxingtu',
btnName: '连接节点'
},
{
fnHandler: 'copyNode',
icoName: 'iconfont icon-fuzhiyemian',
btnName: '复制节点'
},
{
fnHandler: 'deleteNode',
icoName: 'iconfont icon-yincangmima',
btnName: '删除节点'
}
]
},
nodeConnect: {
menuName: 'node-connect-menu',
axis: {
x: null,
y: null
},
menulists: [
{
fnHandler: 'setConnectNode',
icon: nodeJobPath.linkIcon,
icoName: 'iconfont icon-tuodong',
btnName: '连接节点',
nodeConnect: true,
},
{
fnHandler: 'setSerialNode',
icon: nodeJobPath.serial,
icoName: 'iconfont icon-icon-',
btnName: '串行节点',
nodeConnect: true,
},
{
fnHandler: 'setParallelNode',
icon: nodeJobPath.parallel,
icoName: 'iconfont icon-shuxingtu',
btnName: '并行节点',
nodeConnect: true,
},
{
fnHandler: 'setSerialGate',
icon: nodeJobPath.serialGate,
icoName: 'iconfont icon-shuaxin',
btnName: '串行网关',
nodeConnect: true,
},
{
fnHandler: 'setParallelGate',
icon: nodeJobPath.parallelGate,
icoName: 'iconfont icon-tuodong',
btnName: '并行网关',
nodeConnect: true,
}
]
},
link: {
menuName: 'link-menu',
axis: {
x: null,
y: null
},
menulists: [
{
fnHandler: 'setLinkAttr',
icoName: 'iconfont icon-quanjushezhi_o',
btnName: '设置属性'
},
{
fnHandler: 'modifySourceNode',
icoName: 'iconfont icon-step',
icoStyle: 'font-size: 13px',
btnName: '修改起点'
},
{
fnHandler: 'modifyTargetNode',
icoName: 'iconfont icon-radio-off-full',
icoStyle: 'font-size: 13px',
btnName: '修改终点'
},
{
fnHandler: 'deleteLink',
icoName: 'iconfont icon-yincangmima',
btnName: '删除连线'
}
]
},
nodeView: {
menuName: 'node-menu',
axis: {
x: null,
y: null
},
menulists: [
{
fnHandler: '',
icoName: 'iconfont icon-shuaxin',
btnName: '节点名称: '
},
{
fnHandler: '',
icoName: 'iconfont icon-xianshimima',
btnName: '审批开始时间: '
},
{
fnHandler: '',
icoName: 'iconfont icon-gerenzhongxin',
btnName: '参与者: '
},
{
fnHandler: '',
icoName: 'iconfont icon-icon-',
btnName: '审批人: '
},
{
fnHandler: '',
icoName: 'iconfont icon-tongzhi1',
btnName: '审批备注: '
}
]
},
linkView: {
menuName: 'link-menu',
axis: {
x: null,
y: null
},
menulists: [
{
fnHandler: '',
icoName: 'iconfont icon-shuxingtu',
btnName: '连线条件: '
}
]
}
}
}

View File

@@ -0,0 +1,70 @@
import {CommonNodeType, HighNodeType, LaneNodeType} from './type'
import {nodeJobSvgIcons} from "../assets/svges";
export const commonNodes = [
{
type: CommonNodeType.START,
nodeName: '开始',
icon: nodeJobSvgIcons.startMenu
},
{
type: CommonNodeType.SERIAL,
nodeName: '串行节点',
nodeDesc: '(表示流程同时只能流转到一个串行分支)',
icon: nodeJobSvgIcons.serial
},
{
type: CommonNodeType.PARALLEL,
nodeName: '并行节点',
nodeDesc: '(表示流程同时可以流转到多个并行分支)',
icon: nodeJobSvgIcons.parallel
},
{
type: CommonNodeType.END,
nodeName: '结束',
icon: nodeJobSvgIcons.endMenu
}
];
export const highNodes = [
{
type: HighNodeType.VIRTUAL,
nodeName: '虚拟节点',
nodeDesc: '(可选的自由节点,不常用)',
icon: nodeJobSvgIcons.virtual
},
{
type: HighNodeType.JOB,
nodeName: '节点任务',
nodeDesc: '(可选的辅助节点,不常用)',
icon: nodeJobSvgIcons.job
},
{
type: CommonNodeType.SERIAL,
nodeName: '串行网关',
icon: nodeJobSvgIcons.serialGate
},
{
type: CommonNodeType.PARALLEL,
nodeName: '并行网关',
icon: nodeJobSvgIcons.parallelGate
},
{
type: HighNodeType.CHILD_FLOW,
nodeName: '子流程',
icon: 'set-up'
}
];
export const laneNodes = [
{
type: LaneNodeType.Y_LANE,
nodeName: '纵向泳道',
icon: nodeJobSvgIcons.yLaneMenu
},
{
type: LaneNodeType.X_LANE,
nodeName: '横向泳道',
icon: nodeJobSvgIcons.xLaneMenu
}
];

View File

@@ -0,0 +1,110 @@
import { CommonNodeType, HighNodeType, LaneNodeType } from './type'
import {
endAttr, gateAttr,
jobAttr,
laneAttr,
linkAttr,
nodeAttr, startAttr,
virtualAttr
} from "./attr-config";
import JsonflowDesign from "@jackrolling/jsonflow3"
import {deepClone} from "/@/utils/other";
export function initNodeShapes() {
window._defShapes = JsonflowDesign.shapes.initDefShapes();
let initShapes = JsonflowDesign.shapes.initShapes(true);
let cdata = window._defShapes.cdata
initShapes.start.attr(cdata, {
type: CommonNodeType.START,
attrs: deepClone(startAttr),
defJob: deepClone(jobAttr),
})
initShapes.serial.attr(cdata, {
type: CommonNodeType.SERIAL,
attrs: deepClone(nodeAttr),
defJob: deepClone(jobAttr),
})
initShapes.parallel.attr(cdata, {
type: CommonNodeType.PARALLEL,
attrs: deepClone(nodeAttr),
defJob: deepClone(jobAttr),
})
initShapes.serialGate.attr(cdata, {
type: CommonNodeType.SERIAL,
attrs: deepClone(gateAttr),
defJob: deepClone(jobAttr)
})
initShapes.parallelGate.attr(cdata, {
type: CommonNodeType.PARALLEL,
attrs: deepClone(gateAttr),
defJob: deepClone(jobAttr)
})
initShapes.end.attr(cdata, {
type: CommonNodeType.END,
attrs: deepClone(endAttr),
defJob: deepClone(jobAttr),
})
const commonNodes = [
initShapes.start,
initShapes.serial,
initShapes.parallel,
initShapes.end
];
initShapes.virtual.attr(cdata, {
type: HighNodeType.VIRTUAL,
attrs: deepClone(virtualAttr),
defJob: deepClone(jobAttr),
})
initShapes.job.attr(cdata, {
type: HighNodeType.JOB,
defJob: deepClone(jobAttr),
})
const highNodes = [
initShapes.serialGate,
initShapes.parallelGate,
initShapes.virtual,
initShapes.job
/*,
{ cdata: {
type: HighNodeType.CHILD_FLOW,
attrs: deepClone(highAttr)
}
}*/
];
initShapes.xLane.attr(cdata, {
type: LaneNodeType.X_LANE,
attrs: deepClone(laneAttr)
})
initShapes.yLane.attr(cdata, {
type: LaneNodeType.Y_LANE,
attrs: deepClone(laneAttr)
})
const laneNodes = [
initShapes.xLane,
initShapes.yLane
];
const defLink = initShapes.link.attr(cdata, {
type: CommonNodeType.LINK,
attrs: deepClone(linkAttr),
})
return {commonNodes, highNodes, laneNodes, defLink}
}
window._nodeConfig = { initNodeShapes }

View File

@@ -0,0 +1,18 @@
export const CommonNodeType = {
START : 'start',
SERIAL : 'serial',
PARALLEL : 'parallel',
END : 'end',
LINK : 'link'
}
export const HighNodeType = {
CHILD_FLOW : 'child_flow',
VIRTUAL : 'virtual',
JOB : 'job'
}
export const LaneNodeType = {
X_LANE : 'x_lane',
Y_LANE : 'y_lane'
}

View File

@@ -0,0 +1,110 @@
import { flowConfig } from '../config/flow-config.ts'
export let utils = {
seqNo: 1,
getId: function() {
let idType = flowConfig.idType;
if (typeof idType == 'string') {
if (idType === 'uuid') {
return this.getUUID();
} else if (idType === 'time_stamp') {
return this.getTimeStamp();
}
} else if (idType instanceof Array) {
if (idType[0] === 'sequence') {
return this.getSequence(idType[1]);
} else if (idType[0] === 'time_stamp_and_sequence') {
return this.getTimeStampAndSequence(idType[1]);
} else if (idType[0] === 'custom') {
return idType[1]();
}
}
},
getUUID: function() {
let s = [];
let hexDigits = "0123456789abcdef";
for(let i = 0; i < 36; i++) {
s[i] = hexDigits.substr(Math.floor(Math.random() * 0x10), 1);
}
s[14] = "4";
s[19] = hexDigits.substr((s[19] & 0x3) | 0x8, 1);
s[8] = s[13] = s[18] = s[23] = "-";
let uuid = s.join("");
return uuid.replace(/-/g, '');
},
getTimeStamp: function() {
return new Date().getTime();
},
getSequence: function(seqNoLength) {
let zeroStr = new Array(seqNoLength).fill('0').join('');
return (zeroStr + (this.seqNo++)).slice(-seqNoLength);
},
getTimeStampAndSequence: function(seqNoLength) {
return this.getTimeStamp() + this.getSequence(seqNoLength);
},
add: function(a, b) {
let c, d, e;
try {
c = a.toString().split(".")[1].length;
} catch (f) {
c = 0;
}
try {
d = b.toString().split(".")[1].length;
} catch (f) {
d = 0;
}
return e = Math.pow(10, Math.max(c, d)), (this.mul(a, e) + this.mul(b, e)) / e;
},
sub: function(a, b) {
let c, d, e;
try {
c = a.toString().split(".")[1].length;
} catch (f) {
c = 0;
}
try {
d = b.toString().split(".")[1].length;
} catch (f) {
d = 0;
}
return e = Math.pow(10, Math.max(c, d)), (this.mul(a, e) - this.mul(b, e)) / e;
},
mul: function(a, b) {
let c = 0, d = a.toString(), e = b.toString();
try {
c += d.split(".")[1].length;
} catch (f) {}
try {
c += e.split(".")[1].length;
} catch (f) {}
return Number(d.replace(".", "")) * Number(e.replace(".", "")) / Math.pow(10, c);
},
div: function(a, b) {
let c, d, e = 0, f = 0;
try {
e = a.toString().split(".")[1].length;
} catch (g) {}
try {
f = b.toString().split(".")[1].length;
} catch (g) {}
return c = Number(a.toString().replace(".", "")), d = Number(b.toString().replace(".", "")), this.mul(c / d, Math.pow(10, f - e));
},
deepClone(obj) {
let newObj;
try {
newObj = obj.push ? [] : {};
} catch (error) {
newObj = {};
}
for (let attr in obj) {
if (obj[attr] && typeof obj[attr] === 'object') {
newObj[attr] = this.deepClone(obj[attr]);
} else {
newObj[attr] = obj[attr];
}
}
return newObj;
}
};

View File

@@ -0,0 +1,84 @@
<template>
<div>
<el-drawer
class="flow-header-drawer"
title="导入导出"
direction="rtl"
append-to-body
:size="600"
v-model="data.viewJsonVisible"
@close="methods.onClose">
<div style="padding: 24px">
<div>当前JSON数据:</div>
<json-view
:value="data.flowData"
:expand-depth=3
boxed
copyable/>
<div style="margin-top: 12px;">导入导出数据:</div>
<el-input type="textarea" minlength="10" v-model="data.flowDataJson" @change="methods.editFlowDataJson"/>
<el-divider/>
<el-button @click="methods.tempSave" :style="{ marginRight: '8px' }">导出数据</el-button>
<el-button @click="methods.onLoad" type="primary">导入数据</el-button>
</div>
</el-drawer>
</div>
</template>
<script setup lang="ts" name="JsonView">
import JsonView from "vue-json-viewer"
import {useI18n} from "vue-i18n";
import {useMessage} from "/@/hooks/message";
import {stringifyRemoveNullKey} from "../../index";
const {t} = useI18n();
const $message = useMessage();
const $emit = defineEmits(["loadFlow"]);
const data = reactive({
viewJsonVisible: false,
flowData: {},
flowDataJson: ""
})
const methods = {
open(flowData) {
flowData = stringifyRemoveNullKey(flowData);
data.flowData = !window.isWebTest ? {contact: "演示环境不能操作,如需了解联系我们"}: JSON.parse(flowData);
data.viewJsonVisible = true;
},
onClose() {
data.viewJsonVisible = false;
},
// 编辑框
editFlowDataJson(value) {
data.flowDataJson = value;
},
// 导出数据
tempSave() {
if (!data.flowData.attrs) return
let tempObj = Object.assign({}, data.flowData);
data.flowDataJson = stringifyRemoveNullKey(tempObj);
},
// 导入数据
onLoad() {
if (!data.flowDataJson) {
$message.warning("请先导入数据")
return
}
$emit('loadFlow', JSON.parse(data.flowDataJson));
methods.onClose();
}
}
// 暴露变量
defineExpose({
open: methods.open
});
</script>
<style lang="scss">
@import "../../../flow/components/style/flow-drawer.scss";
</style>

View File

@@ -0,0 +1,109 @@
<template>
<div>
<el-drawer
class="flow-header-drawer"
title="画布设置"
direction="rtl"
append-to-body
:size="600"
v-if="data.settingVisible"
v-model="data.settingVisible"
style="z-index: 1001"
@close="methods.close">
<el-form
class="flow-drawer-form"
:model="data.settingForm"
label-position="right">
<el-divider content-position="left">画布</el-divider>
<el-form-item label="网格大小">
<el-slider
:min="1"
:max="20"
v-model="data.gridConfig.gridSize"
@change="methods.setGridSize"/>
</el-form-item>
<el-divider content-position="left">连线</el-divider>
<!-- <el-form-item label="是否允许节点与任务分离显示">
<el-switch
v-model="data.globalConfig.isJobSeparated"
active-value="1"
inactive-value="0"
@change="methods.toggleJobSeparated"/>
</el-form-item>-->
<el-tooltip content="当节点与任务分离显示时,该设置在专业模式下生效" placement="left">
<el-form-item label="是否允许任务连线到其他节点">
<el-switch
v-model="data.globalConfig.allowJobLink"
active-value="1"
inactive-value="0"
@change="methods.toggleAllowJobLink"/>
</el-form-item>
</el-tooltip>
<el-divider content-position="left">样式</el-divider>
<el-form-item label="是否切换节点UI图形显示效果">
<el-switch
v-model="data.globalConfig.isJobSeparated"
active-value="1"
inactive-value="0"
@change="methods.toggleJobSeparated"/>
</el-form-item>
</el-form>
</el-drawer>
</div>
</template>
<script setup lang="ts" name="FlowSetting">
import {flowConfig} from '../config/flow-config'
import * as nodeConfig from "/@/flow/designer/config/node-config";
const $emit = defineEmits(["flowSeparated"]);
const data = reactive({
settingVisible: false,
settingForm: {},
globalConfig: flowConfig.globalConfig,
gridConfig: flowConfig.gridConfig
})
const methods = {
open() {
data.settingVisible = true
},
close() {
data.settingVisible = false
},
toggleAllowJobLink(flag) {
data.globalConfig.allowJobLink = flag
flowConfig.globalConfig.allowJobLink = flag
},
toggleJobSeparated(flag) {
data.globalConfig.isJobSeparated = flag
flowConfig.globalConfig.isJobSeparated = flag
$emit('flowSeparated', flag);
},
setGridSize(v) {
data.gridConfig.gridSize = v
window._jfOperate.resetGridSize(v)
}
}
// 暴露变量
defineExpose({
open: methods.open
});
</script>
<style lang="scss">
@import "../../../flow/components/style/flow-drawer.scss";
.flow-drawer-form {
/*属性面板*/
.el-form-item {
margin: 0 40px 10px;
}
.el-form-item .el-form-item__content {
margin-left: 150px;
}
}
</style>

View File

@@ -0,0 +1,87 @@
<template>
<el-dialog
title="快捷入门"
width="60%"
append-to-body
top="5vh"
v-model="data.modalVisible">
<el-table
row-key="code"
:data="data.dataSource">
<el-table-column
v-for="item in data.columns"
:key="item.dataIndex"
:label="item.title"
:prop="item.key"
style="width: 100%">
</el-table-column>
</el-table>
<template #footer>
<span class="dialog-footer">
<el-button @click="methods.cancel"> </el-button>
<el-button type="primary" @click="methods.saveSetting"> </el-button>
</span>
</template>
</el-dialog>
</template>
<script setup lang="ts" name="FlowShortcut">
const props = defineProps({
shortcut: {
type: Object,
default: null
}
});
const data = reactive({
modalVisible: false,
columns: [
{
title: '功能',
align: 'center',
key: 'shortcutName',
dataIndex: 'shortcutName',
width: '50%'
},
{
title: '快捷入门',
align: 'center',
key: 'codeName',
dataIndex: 'codeName',
width: '50%'
}
],
dataSource: []
})
onMounted(() => {
let obj = Object.assign({}, props.shortcut)
for (let k in obj) {
data.dataSource.push(obj[k])
}
})
const methods = {
open() {
data.modalVisible = true
},
close() {
data.modalVisible = false
},
saveSetting() {
methods.close()
},
cancel() {
methods.close()
}
}
// 暴露变量
defineExpose({
open: methods.open
});
</script>
<style lang="scss" scoped>
</style>

199
src/flow/index.ts Normal file
View File

@@ -0,0 +1,199 @@
import {validateNull} from "/@/utils/validate";
import {ElNotification} from 'element-plus'
import printJS from 'print-js'
import {DIC_PROP} from "/@/flow/support/dict-prop";
import {Local} from "/@/utils/storage";
import FcDesigner from 'form-create-designer';
import {validateFormTypeSave} from "/@/api/order/order-key-vue";
// 判断是否为实例
export const validateRunFlow = (props) => {
if (props.currFlowForm) return props.currFlowForm.flowInstId
else {
if (props.flowData.attrs.orderId) return props.flowData.attrs.id
}
return null
}
// 判断是否为实例
export const validateRunFlowId = (props, form) => {
if (!props.currFlowForm) return form
let flowInstId = validateRunFlow(props);
let id = props.currFlowForm.id;
if (flowInstId) {
let tableName = props.currFlowForm.tableName;
// 判断是否为低代码
form.formId = tableName ? props.currFlowForm.formId: id
form.flowInstId = props.currFlowForm.flowInstId
} else {
form.formId = id
}
return form
}
// 初始化流程表单参数
export const initDefFlowFromAttrs = (props, form) => {
if (!props.currFlowForm) return
// 流程比表单先保存
validateFormTypeSave(props.currFlowForm)
validateRunFlowId(props, form)
// 表单权限主表单首次未入库
form.formName = props.currFlowForm.formName
form.formType = props.currFlowForm.type
form.formPath = props.currFlowForm.path
// 判断是否为低代码
let tableName = props.currFlowForm.tableName;
form.fromType = tableName ? '0' : '1'
}
// 初始化表单必要参数
export const initFromSomeAttrs = (props, form) => {
if (!props.currFlowForm) return
// 字段定义比表单先保存在initDefFlowFromAttrs之后
validateRunFlowId(props, form) // 可能会升版本
form.formName = props.currFlowForm.formName
form.formType = props.currFlowForm.type
form.path = props.currFlowForm.path
}
export function getLabelByLanguage(item) {
let language = Local.get('themeConfig').globalI18n;
return language === 'en' && item.labelEn ? item.labelEn : item.label
}
export function getJobBtnName(jobBtn) {
let item = DIC_PROP.JOB_BTNS.find((f) => f.value === jobBtn);
return getLabelByLanguage(item)
}
export function handleChangeJobType(dicData, item) {
let exist
if (item.jobType === DIC_PROP.JOB_USER_TYPE[0].value) {
exist = !dicData.users ? null : dicData.users.find(f => f.userId === item.roleId);
} else if (item.jobType === DIC_PROP.JOB_USER_TYPE[1].value) {
exist = !dicData.roles ? null : dicData.roles.find(f => f.roleId === item.roleId);
} else if (item.jobType === DIC_PROP.JOB_USER_TYPE[2].value) {
exist = !dicData.posts ? null : dicData.posts.find(f => f.postId === item.roleId);
} else if (item.jobType === DIC_PROP.JOB_USER_TYPE[3].value) {
exist = !dicData.depts ? null : dicData.depts.find(f => f.deptId === item.roleId);
}
// 当不存在情况已选值
if (!exist) item.roleId = null
}
export function handleShowRoleNameByJobType(role, isJobType){
let name = '';
if (isJobType === '0') {
name = role.roleName ? role.roleName.substring(0, role.roleName.indexOf('(')) : ''
} else {
name = role.roleName ? role.roleName + (isJobType ? '(' + getLabelByLanguage(DIC_PROP.JOB_USER_TYPE[1]) + ')' : '') : ''
}
return name
}
export function handleShowNameByJobType(role, isJobType){
let name = '';
if (role.jobType === DIC_PROP.JOB_USER_TYPE[0].value) {
if (isJobType === '0') {
name = role.name ? role.name.substring(0, role.name.indexOf('(')) : ''
} else {
name = role.name ? role.name + (isJobType ? '(' + getLabelByLanguage(DIC_PROP.JOB_USER_TYPE[0]) + ')' : '') : ''
}
} else if (role.jobType === DIC_PROP.JOB_USER_TYPE[1].value) {
name = handleShowRoleNameByJobType(role, isJobType)
} else if (role.jobType === DIC_PROP.JOB_USER_TYPE[2].value) {
if (isJobType === '0') {
name = role.postName ? role.postName.substring(0, role.postName.indexOf('(')) : ''
} else {
name = role.postName ? role.postName + (isJobType ? '(' + getLabelByLanguage(DIC_PROP.JOB_USER_TYPE[2]) + ')' : '') : ''
}
} else if (role.jobType === DIC_PROP.JOB_USER_TYPE[3].value) {
if (isJobType === '0') {
name = role.name ? role.name.substring(0, role.name.indexOf('(')) : ''
} else {
name = role.name ? role.name + (isJobType ? '(' + getLabelByLanguage(DIC_PROP.JOB_USER_TYPE[3]) + ')' : '') : ''
}
}
return name
}
/**
* 请求参数去重,并去掉$与空值
*/
export function paramsFilter(params) {
if (validateNull(params)) return null
let res = {}
let map = new Map(Object.entries(params))
map.forEach((value, key) => {
if (key.includes("$") || validateNull(value)) return
res[key] = value
})
return res
}
export function notifyLeft(msg, type = 'success', time = 2000) {
ElNotification({
title: '提示',
message: msg,
// @ts-ignore
type: type,
// @ts-ignore
position: 'top-left',
duration: time
})
}
export function parseRemoveNullKey(jsonStr) {
const obj = JSON.parse(jsonStr);
return JSON.stringify(obj, (key, value) => {
if (validateNull(value)) {
return undefined; // 返回undefined跳过属性
}
return value;
});
}
export function stringifyRemoveNullKey(object) {
return JSON.stringify(object, (key, value) => {
if (validateNull(value)) {
return undefined; // 返回undefined跳过属性
}
return value;
})
}
export function stringifyWithFunctions(object) {
return FcDesigner.formCreate.toJson(object)
}
export const parseWithFunctions = (obj, func?) => {
if (!func) {
return parseWithString(obj)
}
if (typeof obj === "string") {
return FcDesigner.formCreate.parseJson(obj)
}
return obj;
}
export const parseWithString = (obj) => {
if (typeof obj === "string") {
return JSON.parse(obj);
} else {
return obj;
}
}
export const printHtml = (id, header, data) => {
setTimeout(() => {
printJS({
type: 'html',
printable: id,
header: header,
targetStyles: ["*"],
scanStyles: true,
maxWidth: 1100,
})
if (data) data.width = "-"
}, 100)
}

View File

@@ -0,0 +1,64 @@
/**
* top顶端待办任务
*/
import { defineStore } from 'pinia';
import {fetchTodoSize} from '/@/api/jsonflow/do-job'
import {Local} from "/@/utils/storage";
import {topJobList} from "./index";
import {locationHash} from "/@/api/order/order-key-vue";
import {windowLocationHref} from "/@/flow/support/extend";
interface Job {
jobLen: number | null
}
// 是否开启JsonFlow
const jsonFlowEnable = ref(import.meta.env.VITE_JSON_FLOW_ENABLE === 'true');
export const useFlowJob = defineStore('flow/job', {
state: (): Job => ({
jobLen: Local.get('jobLen') || 0
}),
actions: {
jsonFlowEnable() {
return jsonFlowEnable.value;
},
topJobList() {
if (this.jsonFlowEnable()) topJobList();
},
onTodoJobClick() {
// 因a标签href="#",会先跳转/
setTimeout(() => {
windowLocationHref(locationHash.TodoJobHash)
}, 0)
},
// 获取待办任务数
getJobList() {
if (!this.jsonFlowEnable()) return Promise.resolve();
return new Promise((resolve, reject) => {
fetchTodoSize({belongType: '-1', current: 1, size: 1})
.then(response => {
this.jobLen = response.data.total
Local.set('jobLen', this.jobLen);
resolve(this.jobLen)
}).catch(error => {
reject(error)
})
})
},
setJobLen(len){
this.jobLen = len
Local.set('jobLen', this.jobLen);
},
async addJobLen() {
await this.getJobList()
},
async delJobLen() {
await this.getJobList()
},
clearJobLen(){
this.jobLen = 0
Local.set('jobLen', 0);
}
}
});

72
src/flow/stores/index.ts Normal file
View File

@@ -0,0 +1,72 @@
import {useFlowJob} from "/@/flow/stores/flowJob";
import {ElNotification} from "element-plus";
import {flowConfig} from "/@/flow/designer/config/flow-config";
// 登录通知待办任务数
export function topJobList() {
// 兼容移动端
const route = useRoute();
let index = route.fullPath.indexOf(flowConfig.mobileConfig.mobilePrefix);
if (index !== -1) return
useFlowJob().getJobList().then((jobLen) => {
if (!jobLen || jobLen === 0) return
ElNotification({
title: "总任务数提醒",
type: 'warning',
dangerouslyUseHTMLString: true,
message: `<a href="#" style="color:#FF4500;"><i class="iconfont icon-xianshimima"></i>当前总共&nbsp;${jobLen}&nbsp;条待办任务,请及时处理!</a>`,
position: 'top-right',
offset: 60,
onClick: () => {
useFlowJob().onTodoJobClick();
}
})
})
}
// 消息通知工作流任务
export function notifyJsonFlowJob(res) {
const data = JSON.parse(res.data)
// 延迟解决可能没有工单数据
setTimeout(() => {
handleJsonFlowJob(data)
}, 6000)
}
// 任务状态判断
function handleJsonFlowJob(data) {
const opts: any = {}
if (data.status === '0') { // 运行中
const jobType = data.jobType === '0' ? '个人任务:' : '组任务:'
const type = 'warning'
const msg = '<a href="#" style="color:#FF4500;">' +
'<i class="iconfont icon-xianshimima"></i>&nbsp;' + jobType + data.data + '</a>' + ',请及时处理'
opts.onClick = function () {
useFlowJob().onTodoJobClick();
}
jobNotify(opts, type, msg)
} else { // 发起 或 结束
const text = data.status === '-1' ? data.data : data.data + '已完成, 请悉知'
const type = data.status === '-1' ? 'warning' : 'success'
const msg = '<a href="#" style="color:#FF4500;">' + text + '</a>'
jobNotify(opts, type, msg)
}
}
// 任务提醒
function jobNotify(opts, type, msg) {
useFlowJob().addJobLen();
const defOpts = {
title: '任务提醒',
type: type,
dangerouslyUseHTMLString: true,
message: msg,
offset: 60
}
ElNotification(Object.assign(defOpts, opts))
}
export function isJSON(str) {
const jsonRegex = /^\s*[\[{].*[\]}]\s*$/;
return jsonRegex.test(str);
}

View File

@@ -0,0 +1,98 @@
/**
* 业务公共处理类
* @author luolin
*/
import {validateNull} from "/@/utils/validate";
import {deepClone} from "/@/utils/other";
// 校验是否为对象或数组
export function validateObjArr(objArr) {
return objArr instanceof Array || objArr instanceof Object;
}
// 处理克隆
export function handleClone(_this, form: any, ...keys?: string[]) {
if (_this.operType !== 'add' && _this.operType !== 'copy') return
if (!form) form = _this.form
setPropsNull(form, 'code', 'status', 'finishTime', 'createUser', 'createTime', 'updateUser', 'updateTime')
// 自定义清空字段
setPropsNull(form, ...keys)
return form
}
// 处理克隆提交前
export function handleCloneSubmit(_this, row?: any, callback?: Function) {
if (_this.operType !== 'add' && _this.operType !== 'copy') return
if (!row) row = _this.form
row = handleClone(_this, row)
setPropsNull(row, 'id', 'flowInstId')
if (callback) callback(row)
}
// 设置属性值null
// @ts-ignore
export function setPropsNull(form: any, ...keys?: string[]) {
if (arguments.length < 2) return
let args = Array.prototype.slice.call(arguments)
for (let i = 1; i < args.length; i++) {
form[args[i]] = null
}
}
// 统一设置属性值
// @ts-ignore
export function setPropsValue(form: any, value: any, ...keys?: string[]) {
if (arguments.length < 3) return
let args = Array.prototype.slice.call(arguments)
for (let i = 2; i < args.length; i++) {
form[args[i]] = value
}
}
// 重置表单数据
// @ts-ignore
export function setPropsNullValue(form: any, data: any, ...keys?: string[]) {
if (arguments.length < 3) {
keys = Object.keys(form)
}
setPropsNull(form, ...keys)
if (validateNull(data)) return
setPropsDataValue(form, data, keys)
}
// 设置属性为data值
// @ts-ignore
export function setPropsDataValue(form, data, keys?: string[]) {
if (arguments.length < 3) {
let keys = Object.keys(data);
for (let i = 2; i < keys.length; i++) {
if (!validateNull(data[keys[i]])) form[keys[i]] = data[keys[i]]
}
return
}
let args = Array.prototype.slice.call(arguments)
// 第三个参数是否为数组
if (args[2] instanceof Array) {
args[2].forEach(key => {
if (!validateNull(data[key])) {
// 防止相同引用
if (validateObjArr(data[key])) form[key] = deepClone(data[key])
else form[key] = data[key]
}
})
} else {
for (let i = 2; i < args.length; i++) {
if (!validateNull(data[args[i]])) form[args[i]] = data[args[i]]
}
}
}
// 设置Crud queryForm
export function setCrudQueryForm(_this, ...params?) {
if (arguments.length < 2) return
let args = Array.prototype.slice.call(arguments)
// 单个修改
for (let i = 1; i < args.length; i++) {
_this.queryForm[args[i].prop] = args[i].value
}
}

View File

@@ -0,0 +1,782 @@
/**
* 字典属性常量
* @author luolin
*/
let paramValTypes = [{
label: '表单字段',
value: '0',
}, {
label: 'SpEL表达式',
value: '1',
}, {
label: '固定值',
value: '2',
}]
let jobTypes = [{
label: '人员',
value: '0',
labelEn: 'User'
}, {
label: '角色',
value: '1',
labelEn: 'Role'
}, {
label: '岗位',
value: '2',
labelEn: 'Post'
}, {
label: '部门',
value: '3',
labelEn: 'Dept'
}]
let methods = [{
label: '当开启任务时',
value: 'startJob'
}, {
label: '当完成任务时',
value: 'completeJob'
}, {
label: '当开启下一步任务时',
value: 'startNextJob'
}, {
label: '当开启节点时',
value: 'startNode'
}, {
label: '当完成节点时',
value: 'completeNode'
}, {
label: '当开启下一步节点时',
value: 'startNextNode'
}, {
label: '当任务跳转时',
value: 'anyJump'
}, {
label: '当任务被跳转时',
value: 'anyJumped'
}, {
label: '当任务驳回时',
value: 'reject'
}, {
label: '当任务被驳回时',
value: 'rejected'
}, {
label: '当需分配参与者时',
value: 'distPerson'
}, {
label: '退回首节点时',
value: 'backFirst'
}, {
label: '退回上一步时',
value: 'backPre'
}]
export let DIC_PROP = {
YES_OR_NO: [{
label: '否',
value: '0',
labelEn: 'No'
}, {
label: '是',
value: '1',
labelEn: 'Yes'
}],
YES_OR_NO_BOOL: [{
label: '否',
value: false,
labelEn: 'No'
}, {
label: '是',
value: true,
labelEn: 'Yes'
}],
YES_OR_NO_NUM: [{
label: '否',
value: 0,
labelEn: 'No'
}, {
label: '是',
value: 1,
labelEn: 'Yes'
}],
DELEGATE_STATUS: [{
label: '暂存',
value: '-1',
labelEn: 'Temp'
}, {
label: '启用',
value: '0',
labelEn: 'Enable'
}, {
label: '停用',
value: '1',
labelEn: 'Disable'
}],
TEMP_STATUS: [{
label: '暂存',
value: '-1',
labelEn: 'Temp'
}, {
label: '作废',
value: '0',
labelEn: 'Invalid'
}, {
label: '发布',
value: '1',
labelEn: 'Publish'
}],
ORDER_STATUS: [{
label: '撤回',
value: '-2'
}, {
label: '暂存',
value: '-1'
}, {
label: '运行中',
value: '0'
}, {
label: '完成',
value: '1'
}, {
label: '作废',
value: '2'
}, {
label: '终止',
value: '3'
}],
HANDOVER_TYPE: [{
label: '任务交接',
value: '-1'
}, {
label: '可自定义更多',
value: '0'
}],
HANDOVER_REASON: [{
label: '日常交接',
value: '-1'
}, {
label: '晋升',
value: '0'
}, {
label: '转岗',
value: '1'
}, {
label: '离职',
value: '2'
}, {
label: '平调',
value: '3'
}],
HANDOVER_STATUS: [{
label: '撤回',
value: '-2'
}, {
label: '未交接',
value: '-1'
}, {
label: '交接中',
value: '0'
}, {
label: '已交接',
value: '1'
}, {
label: '作废',
value: '2'
}, {
label: '终止',
value: '3'
}],
NODE_TYPE: [
{
label: '开始节点',
value: '-1'
}, {
label: '串行节点',
value: '0'
}, {
label: '并行节点',
value: '1'
}, {
label: '结束节点',
value: '2'
}, {
label: '虚拟节点',
value: '3'
}, {
label: '横向泳道',
value: '8'
}, {
label: '纵向泳道',
value: '9'
}
],
LINK_TYPE: [
{
label: '节点到节点',
value: '0'
}, {
label: '节点到任务',
value: '1'
}
],
FLOW_METHOD_TYPE: [{
label: '发起人本人',
value: '0'
}, {
label: '单级部门主管',
value: '1'
}, {
label: '多级部门主管',
value: '2'
}, {
label: '指定部门主管',
value: '3'
}, {
label: '表单内人员',
value: '4'
}, {
label: '可自定义更多',
value: '-1'
}],
JOB_MSG_TYPE: [{
label: '个人任务',
value: '0'
}, {
label: '组任务',
value: '1'
}],
JOB_USER_TYPE: jobTypes,
JOB_USER_NONE_TYPE: [{
label: '无',
value: '-1',
labelEn: 'None'
},
...jobTypes
],
NODE_APPROVE_METHOD: [{
label: '会签(需所有节点通过)',
value: '1'
}, {
label: '或签(一个节点通过即可)',
value: '2'
}, {
label: '依次审批(按顺序依次通过)',
value: '3'
}],
APPROVE_METHOD: [{
label: '会签(需所有审批人同意)',
value: '1'
}, {
label: '或签(一名审批人同意即可)',
value: '2'
}, {
label: '依次审批(按顺序依次审批)',
value: '3'
}, {
label: '票签(自定义完成比率%)',
value: '4'
}],
SIGNATURE_TYPE: [{
label: '前加签',
value: '1',
labelEn: 'Before Signature'
}, {
label: '后加签',
value: '2',
labelEn: 'After Signature'
}, {
label: '加并签',
value: '3',
labelEn: 'Sign Together'
}],
NODE_SEQUENTIAL_TYPE: [{
label: '在当前节点之前审批',
value: '1'
}, {
label: '在当前节点之后审批',
value: '2'
}, {
label: '与当前节点同时审批',
value: '3'
}],
NODE_SIGNATURE_TYPE: [{
label: '前加节点',
value: '1'
}, {
label: '后加节点',
value: '2'
}, {
label: '加并节点',
value: '3'
}],
BELONG_TYPE: [{
label: '普通任务',
value: '0',
labelEn: 'Common Task'
}, {
label: '抄送任务',
value: '1',
labelEn: 'Copy Task'
}, {
label: '传阅任务',
value: '2',
labelEn: 'Pass task'
}],
JOB_BTNS: [{
label: '同意',
value: '0',
labelEn: 'Agree'
}, {
label: '前加节点',
value: '13',
labelEn: 'Before Node'
}, {
label: '后加节点',
value: '14',
labelEn: 'After Node'
}, {
label: '加并节点',
value: '15',
labelEn: 'Node Together'
}, {
label: '前加签',
value: '5',
labelEn: 'Before Signature'
}, {
label: '后加签',
value: '6',
labelEn: 'After Signature'
}, {
label: '加并签',
value: '7',
labelEn: 'Sign Together'
}, {
label: '退回首节点',
value: '1',
labelEn: 'Back First'
}, {
label: '退回上一步',
value: '10',
labelEn: 'Back Pre'
}, {
label: '任意跳转',
value: '8',
labelEn: 'Any Jump'
}, {
label: '任意驳回',
value: '2',
labelEn: 'Any Reject'
}, {
label: '抄送任务',
value: '11',
labelEn: 'Carbon Copy'
}, {
label: '传阅任务',
value: '12',
labelEn: 'Pass Read'
}, {
label: '下一办理人',
value: '3',
labelEn: 'Next Handler'
}, {
label: '转办任务',
value: '17',
labelEn: 'Turn Job'
}, {
label: '终止流程',
value: '18',
labelEn: 'Terminate'
}, {
label: '提前结束',
value: '9',
labelEn: 'Early End'
}, {
label: '作废流程',
value: '4',
labelEn: 'Invalid'
}, {
label: '分配参与者',
value: '-9',
labelEn: 'Dist Person'
}],
ROUTE_ACTIONS: [{
label: '暂停当前节点',
value: '-4',
labelEn: 'STop Current Node'
}, {
label: '开启下一节点',
value: '8',
labelEn: 'Start Next Node'
}, {
label: '通过',
value: '0',
labelEn: 'Agree'
}, {
label: '驳回到其他节点',
value: '2',
labelEn: 'Reject To Node'
}, {
label: '退回首节点',
value: '1',
labelEn: 'Back First'
}, {
label: '退回上一步',
value: '10',
labelEn: 'Back Pre'
}, {
label: '终止流程',
value: '18',
labelEn: 'Terminate'
}, {
label: '提前结束',
value: '9',
labelEn: 'Early End'
}, {
label: '可自定义更多',
value: '_define_',
labelEn: '_define_'
}],
MSG_TYPE: [{
label: '个人消息',
value: '0'
}, {
label: '群消息',
value: '1'
}],
NODE_STATUS: [{
label: '未开始',
value: '-1',
labelEn: 'No Start'
}, {
label: '办理中',
value: '0',
labelEn: 'Running'
}, {
label: '完成',
value: '1',
labelEn: 'Complete'
}, {
label: '驳回中',
value: '2',
labelEn: 'Rejecting'
}, {
label: '跳过',
value: '3',
labelEn: 'Skip'
}, {
label: '被驳回',
value: '9',
labelEn: 'Rejected'
}],
REJECT_STATUS: [{
label: '驳回中',
value: '0'
}, {
label: '结束',
value: '1'
}],
REJECT_TYPE: [{
label: '依次返回',
value: '0'
}, {
label: '直接返回',
value: '1'
}],
OPERATOR: [{
label: '等于',
value: '0'
}, {
label: '不等于',
value: '1'
}, {
label: '大于',
value: '2'
}, {
label: '大于等于',
value: '3'
}, {
label: '小于',
value: '4'
}, {
label: '小于等于',
value: '5'
}, {
label: '包含',
value: '6'
}, {
label: '不包含',
value: '7'
}],
VAL_TYPE: [{
label: '普通模式',
value: '-2'
}, {
label: '分配模式',
value: '-1'
}, {
label: '简单模式',
value: '0'
}, {
label: 'SpEL模式',// 针对人员为固定模式
value: '1'
}, {
label: '专业模式',
value: '2'
}, {
label: 'Http模式',
value: '3'
}],
NOTICE_STATUS: [{
label: '发起',
value: '-1'
}, {
label: '办理中',
value: '0'
}, {
label: '完成',
value: '1'
}],
TODO_JOB_STATUS: [{
label: '未开始',
value: '-1'
}, {
label: '办理中',
value: '0'
}, {
label: '驳回中',
value: '2'
}, {
label: '被驳回',
value: '9'
}],
FLOW_STATUS: [{
label: '撤回',
value: '-2',
labelEn: 'Recall'
}, {
label: '发起',
value: '-1',
labelEn: 'Initiate'
}, {
label: '运行中',
value: '0',
labelEn: 'Running'
}, {
label: '完结',
value: '1',
labelEn: 'End'
}, {
label: '作废',
value: '2',
labelEn: 'Invalid'
}, {
label: '终止',
value: '3',
labelEn: 'Terminate'
}],
NODE_METHODS: methods,
FLOW_METHODS: [{
label: '当流程发起时',
value: 'initiate'
}, {
label: '当流程完成时',
value: 'finish'
}, {
label: '当流程被撤回时',
value: 'recall'
}, {
label: '当流程被重发时',
value: 'reset'
}, {
label: '当流程被终止时',
value: 'terminate'
}, {
label: '当流程被作废时',
value: 'invalid'
}, {
label: '当流程被恢复时',
value: 'recover'
}, ...methods],
CLAZZ_TYPE: [{
label: '节点事件',
value: '0'
}, {
label: '全局事件',
value: '1'
}],
COLUMN_TYPE: [
{
label: "varchar",
value: "varchar"
},{
label: "bigint",
value: "bigint"
},{
label: "int",
value: "int"
},{
label: "double",
value: "double"
},{
label: "timestamp",
value: "timestamp"
},{
label: "date",
value: "date"
},{
label: "float",
value: "float"
},{
label: "bit",
value: "bit"
}
],
LOCK_FLAG: [{
label: '有效',
value: '0'
}, {
label: '锁定',
value: '9'
}],
DATABASE_TYPE: [{
label: 'MySQL数据库',
value: 'MySQL'
}, {
label: '达梦数据库',
value: 'DM'
}],
FORM_TYPE: [{
label: '设计表单',
value: '0'
}, {
label: '系统表单',
value: '1'
}],
FORM_DATA_TYPE: [{
label: '字段定义',
value: '0'
}, {
label: '权限配置',
value: '1'
}, {
label: '打印设计',
value: '2'
}],
FORM_PERM_TYPE: [{
label: '隐藏',
value: '-1'
}, {
label: '只读',
value: '0'
}, {
label: '可编辑',
value: '1'
}],
ALL_FORM_PERM_TYPE: [{
label: '全部只读',
value: '0'
}, {
label: '全部可编辑',
value: '1'
}],
PARAM_RULE_TYPE: [{
label: '人员规则',
value: '0'
}, {
label: '条件规则',
value: '1'
}, {
label: '父子流程',
value: '2'
}, {
label: '监听事件',
value: '3'
}, {
label: '查询/更新表单Http参数',
value: '4'
}, {
label: '保存子流程表单Http接口',
value: '5'
}, {
label: '更新子流程表单Http接口',
value: '6'
}, {
label: '更新父流程表单Http接口',
value: '7'
}, {
label: '节点路由',
value: '8'
}],
OR_OR_AND: [{
label: '或',
value: '0',
}, {
label: '且',
value: '1',
}],
PARAM_FROM: [{
label: '请求头',
value: '0',
}, {
label: '传参',
value: '1',
}, {
label: '回参',
value: '2',
}],
PARAM_VAL_TYPE: paramValTypes,
SYS_PARAM_VAL_TYPE: [{
label: '系统字段',
value: '0',
}, ...paramValTypes.slice(1)
],
HTTP_METHODS: [{
label: 'GET',
value: 'GET',
}, {
label: 'POST',
value: 'POST',
}, {
label: 'PUT',
value: 'PUT',
}, {
label: 'DELETE',
value: 'DELETE',
}],
PARAM_TYPES: [{
label: 'json',
value: '0',
}, {
label: 'form',
value: '1',
}],
PREDICT_OPTIONS: [{
label: '预测参与者',
value: 1,
}, {
label: '预测监听事件',
value: 2,
}, {
label: '预测节点路由',
value: 4,
}, {
label: '预测子流程',
value: 8
}, {
label: '预测审批过程',
value: 16,
}, {
label: '预测非自动流转',
value: 32,
}, {
label: '预测流程轨迹',
value: 0,
disabled: true
}, {
label: '预测连线条件',
value: 0,
disabled: true
}],
}

255
src/flow/support/extend.ts Normal file
View File

@@ -0,0 +1,255 @@
import {RouteRecordRaw} from "vue-router";
import {flowConfig} from "/@/flow/designer/config/flow-config";
import {Session} from "/@/utils/storage";
import {locationHash, vueKeySys} from "/@/api/order/order-key-vue";
import {useUserInfo} from "/@/stores/userInfo";
import {validateNull} from "/@/utils/validate";
import {getToDoneDetail} from "/@/api/jsonflow/hi-job";
import * as doJob from "/@/api/jsonflow/do-job";
import {DIC_PROP} from "/@/flow/support/dict-prop";
import {paramsFilter} from "/@/flow";
// 移动端path
export const vueAppKeyHash = {
flowview: 'jsonflow/flow-design/view'
, jobview: 'order/run-application/view'
, jobinitiate: 'order/flow-application/initiate'
, jobinitagain: 'order/run-application/initiate'
, jobform: 'order/run-application/flow'
}
export async function initJobDataByApp($route, props) {
let appdata = flowConfig.mobileConfig.mobilePrefix
let app = !validateNull($route.query[appdata]);
if (!app) return
let split = $route.query[appdata].split('-');
let query = {orderId: split[1], id: split[2], flowInstId: split[3], runNodeId: split[4], elTabId: split[5], hiJob: split[6] === 'Y'}
if (query.hiJob) {
await handleToDoneDetail(query, props, '1')
} else {
await handleTodoDetail(query, props)
}
props.currJob.hiJob = query.hiJob
props.currElTab.id = query.elTabId
}
export async function handleTodoDetail(row, data, isView?, isRead?, $message?, getDataList?) {
let query = Object.assign({}, row, {order: null, elTabs: null});
query = paramsFilter(query)
await doJob.getTodoDetail(query).then(response => {
if ($message) data.currJob = response.data
// 兼容移动端
else Object.assign(data.currJob, response.data);
if (isRead !== '1' && isView !== '1' && DIC_PROP.NODE_STATUS[2].value === data.currJob.status) {
if ($message) {
$message.warning('当前任务已被其他办理人审批')
getDataList();
}
} else {
if (isView !== '1') {
if (isRead === '1') data.currJob.hiJob = true
doJob.isRead(data.currJob).then(() => {
row.isRead = '1'
// 独立标签都需执行
if (isRead === '1') getDataList();
})
} else data.currJob.hiJob = true
}
})
}
const channel = new BroadcastChannel('currJob_channel');
export async function handleFlowPreview($router, row, isView?, isRead?, $message?, flowJob?, getDataList?) {
channel.onmessage = (event) => {
flowJob.delJobLen();
// getDataList();
};
row.isHiJob = '0'
windowOpenFlow($router, row, isView, isRead)
}
function windowOpenFlow($router, row, isView?, isRead?, isApp?, isForm?) {
let query = { id: row.id, flowInstId: row.flowInstId, isHiJob: row.isHiJob, isView, isRead, isApp, isForm }
$router.push({
name: dynamicRoutesFlow[0].name,
query: query
})
/*const routeUrl = $router.resolve({
path: dynamicRoutesFlow[0].path,
query: query,
});
window.open(routeUrl.href, '_blank');*/
}
export async function handleToDoneDetail(row, data, isApp?, isForm?) {
let query = Object.assign({}, row, {order: null, elTabs: null});
query = paramsFilter(query)
await getToDoneDetail(query).then(response => {
// 兼容移动端
if (isApp === '1') {
Object.assign(data.currJob, response.data);
return
}
data.currJob = response.data
if (isForm === '1') {
let elTabs = data.currJob.elTabs;
data.currJob.elTabs = elTabs.filter(f => vueKeySys.sysPaths.includes(f.path))
// 判断是否只配置表单
if (validateNull(data.currJob.elTabs)) data.currJob.elTabs = elTabs
}
// 历史查看标识
data.currJob.hiJob = true
})
}
export async function openFlowPreview($router, row, isForm?) {
row.isHiJob = '1'
windowOpenFlow($router, row, null, null, null, isForm)
}
export async function replaceRouterRoute(route, router) {
// 兼容移动端
let appPath = '', appdata = flowConfig.mobileConfig.mobilePrefix;
// 防止重复进入
if (route.fullPath.indexOf('?' + appdata + '=') !== -1) {
return
}
let index = route.fullPath.indexOf(appdata);
if (index !== -1) {
let last = route.fullPath.indexOf("-");
if (route.fullPath.indexOf('?' + appdata + '=') !== -1) last = route.fullPath.indexOf("?");
appPath = route.fullPath.substring(route.fullPath.indexOf("/") + 1, last);
router.currentRoute.value.name = "flowRoutes." + appPath
let fullPath = route.fullPath.replace('-', '?');
fullPath = fullPath.replace('-', '=');
fullPath = fullPath.replace(appPath, vueAppKeyHash[appPath]);
let split = fullPath.split('=')[1].split('-');
// 存储token 信息
if (split[0].indexOf('_') !== -1) {
// 判断是否微服务版本
let tokenTenant = split[0].replace('_', '-').split('-')
Session.set('tenantId', tokenTenant[0]);
Session.set('token', tokenTenant[1].replaceAll('_', '-'));
} else {
Session.set('token', split[0]);
}
// 触发初始化用户信息 pinia
await useUserInfo().setUserInfos();
await router.replace(fullPath);
}
}
export async function replaceRouterThirdRoute(route, router) {
let fullPath = route.fullPath;
// 防止重复进入
if (fullPath.indexOf('tokenLoaded') !== -1) {
// 防止刷新后无用户信息
await useUserInfo().setUserInfos();
return
}
// 三方系统单点
if (fullPath.indexOf('token') !== -1) {
Session.clear()
fullPath = fullPath.replace('token', 'tokenLoaded');
// 存储token 信息
Session.set('tenantId', route.query.tenantId);
Session.set('token', route.query.token);
} else {
fullPath = route.fullPath + '&tokenLoaded=1';
}
// 必须获取用户信息
await useUserInfo().setUserInfos();
router.currentRoute.value.name = "staticRoutes.handleoajob"
await router.replace(fullPath);
}
export function initJsonFlowViewByApp($route, props) {
let appdata = flowConfig.mobileConfig.mobilePrefix
let app = !validateNull($route.query[appdata]);
if (app) {
let split = $route.query[appdata].split('-');
let query = {defFlowId: split[1], flowInstId: split[2] === 'undefined' ? '' : split[2]}
Object.assign(props.currJob, query);
}
}
export function windowLocationHrefParam(hash, param = '') {
return window.location.href = window.location.origin + window.location.pathname + locationHash[hash] + param
}
export function windowLocationHref(hash) {
window.location.href = window.location.origin + window.location.pathname + hash
}
/**
* 定义静态路由(默认路由)
*/
export const staticRoutesFlow: Array<RouteRecordRaw> = [
{
path: '/jsonflow/flow-design/view',
name: 'flowRoutes.flowview',
component: () => import('/@/views/jsonflow/flow-design/view.vue'),
meta: {
isAuth: false,
},
},
{
path: '/order/run-application/view',
name: 'flowRoutes.jobview',
component: () => import('/@/views/order/run-application/view.vue'),
meta: {
isAuth: false,
},
},
{
path: '/order/flow-application/initiate',
name: 'flowRoutes.jobinitiate',
component: () => import('/@/views/order/flow-application/initiate.vue'),
meta: {
isAuth: false,
},
},
{
path: '/order/run-application/initiate',
name: 'flowRoutes.jobinitagain',
component: () => import('/@/views/order/run-application/initiate.vue'),
meta: {
isAuth: false,
},
},
{
path: '/order/run-application/flow',
name: 'flowRoutes.jobform',
component: () => import('/@/views/order/run-application/flow.vue'),
meta: {
isAuth: false,
},
}
];
/**
* 定义静态路由(默认路由)
* 前端添加路由,请在此处加
*/
export const dynamicRoutesFlow: Array<RouteRecordRaw> = [
{
path: '/flow/components/handle-job/handle',
name: 'flowRoutes.handlejob',
component: () => import('/@/flow/components/handle-job/handle.vue'),
meta: {
isKeepAlive: false,
isHide: true,
},
},
{
path: '/jsonflow/flow-design/index',
name: 'flowRoutes.flowdesign',
component: () => import('/@/views/jsonflow/flow-design/index.vue'),
meta: {
isKeepAlive: false,
isHide: true,
},
},
];

View File

@@ -0,0 +1,85 @@
/**
* 字段属性常量
* @author luolin
*/
const varKeyVal = {
sys: '#sys.',
flow: '#flow.',
order: '#order.',
form: '#form.',
user: '#user.',
var: '#var.',
dist: '#dist.',
}
export let PROP_CONST = {
COMMON: {
flowInstId: 'flowInstId',
code: 'code',
userUserPrefix: 'USER_',
userRolePrefix: 'ROLE_',
userPostPrefix: 'POST_',
userDeptPrefix: 'DEPT_',
tableName: 'order_run_application',
},
FORM_DESIGN: {
// 注意相反的!
subForm: 'group',
group: 'subForm',
fcRow: 'fcRow',
tableForm: 'tableForm',
fcTable: 'fcTable',
elTabs: 'elTabs',
elCollapse: 'elCollapse',
elCard: 'elCard',
},
TEXT_DESC: {
condSpELExplain: "当选择SpEL模式时, SpEL表达式必须符合SpEL格式, #anyKey表示表单的字段 ( 默认#form.前缀 ), 例如0<#days && #days>=3 ( 加前缀#var.anyKey表示从流程条件中取值#user.anyKey表示从当前用户中取值 )",
condUserExplain: "1、SpEL上下文表达式 ( 常用于发起时可确定的参与者值 ), #anyKey表示表单的字段 ( 默认#form.前缀 ), 例如#userId ( 加前缀#var.anyKey表示从流程条件中取值#user.anyKey表示从当前用户中取值 )",
condMethodExplain1: "采用函数表达式 ( 以下两种方式均支持自定义任意扩展 ), 返回值为字符串 1 ( 满足 ) 或 0 ( 不满足 ), 满足您复杂条件的场景 :",
condMethodExplain2: "1、SpEL上下文表达式, #anyKey表示表单的字段 ( 默认#form.前缀 ), 例如#isGoEnd ( 加前缀#var.anyKey表示从流程条件中取值#user.anyKey表示从当前用户中取值 )",
condMethodExplain3: '2、SpringBean函数表达式, 如某个Bean对象的beanName为bean取值#bean.anyMethod(), 带参数格式#bean.anyMethod(String#admin,SysUser#{"username": "admin"},SysRole#NULL)',
condMethodExplain4: '备注:函数表达式 ( 参数支持SpEL上下文表达式且需带前缀如Long#form.userId ), 参数格式为#bean.anyMethod(参数类型#参数值), 多个参数逗号分割。类型为复杂对象时参数值为Json格式',
condMethodExplain5: '字段可包含参与者类型、参与者ID、参与者任务名称、参与者任务排序( 只有ID值时参与者类型默认人员参与者ID是人员ID )',
condMethodExplain6: '返回值要求请参考路由指定动作接口的入参',
},
HANDOVER_FLOW: {
userKey: {
create_user:'create_user'
,receive_user:'receive_user'
,curr_dept_manager:'curr_dept_manager'
}
},
LOAD_USER_ROLE: [
{key: "users", type: "jobType", typeVal: "0", field: 'roleId'}, {key: "roles", type: "jobType", typeVal: "1", field: 'roleId'},
{key: "posts", type: "jobType", typeVal: "2", field: 'roleId'}, {key: "depts", type: "jobType", typeVal: "3", field: 'roleId'}
],
SYS_FIELDS: [
// 用 order 简化报表打印回显
{prefix: varKeyVal.order, prop:"flowInstId", label:"流程实例ID", valueKey: "id", showKey: "flowName"}
,{prefix: varKeyVal.order, prop:"flowKey", label:"流程业务KEY"}
,{prefix: varKeyVal.order, prop:"createUser", label:"发起人", valueKey: "userId", showKey: "name"}
,{prefix: varKeyVal.order, prop:"code", label:"工单编号"}
,{prefix: varKeyVal.order, prop:"status", label:"工单状态"}
,{prefix: varKeyVal.order, prop:"createTime", label:"发起时间"}
,{prefix: varKeyVal.order, prop:"finishTime", label:"完成时间"}
,{prefix: varKeyVal.order, prop:"_define_", label:"可自定义更多"}
],
VAR_KEY_VAL: {
route: '#route.',
routeName: '路由规则',
person: '#person.',
personName: '审批规则',
link: '#link.',
// 取值来源
order: varKeyVal.order,
form: varKeyVal.form,
user: varKeyVal.user,
var: varKeyVal.var,
dist: varKeyVal.dist,
},
FLOW_METHOD: {
whoseLeader: {name: '发起人本人', userId: varKeyVal.order.replace("#", "") + 'createUser'}
},
}

380
src/flow/utils/form-perm.ts Normal file
View File

@@ -0,0 +1,380 @@
import {listFormOption, listPrintTemp, listStartPerm} from "/@/api/jsonflow/form-option";
import {DIC_PROP} from "/@/flow/support/dict-prop";
import {useMessage} from "/@/hooks/message";
import {validateNull} from "/@/utils/validate";
import {PROP_CONST} from "/@/flow/support/prop-const";
import {deepClone} from "/@/utils/other";
import {validateRunFlow} from "/@/flow";
// 处理表单权限
export async function handleFormStartPerm(hiddenFields, disabledFields, formInfo, defFlowId, flowKey, formType) {
if (!formType) formType = DIC_PROP.FORM_TYPE[1].value
let resp = await listStartPerm({
type: DIC_PROP.FORM_DATA_TYPE[1].value, formType: formType,
defFlowId: defFlowId, flowKey: flowKey
}).catch(() => {
useMessage().error("获取表单字段权限失败");
})
let data = resp?.data;
// 表单默认权限及查看时查询打印模板
if (validateNull(data.columns)) return {elTab: data.tabsOption};
let widgetList = formInfo ? formInfo.widgetList : null
// 全部只读部分可编辑
let callback = () => { handleFormPerms(hiddenFields, disabledFields, widgetList, data.tabsOption, data.columns, data.defFlowId, data.flowNodeId) }
initIsFormEdit(data.tabsOption, data.columns, data.defFlowId, data.flowNodeId)
return {elTab: data.tabsOption, callback, widgetList};
}
export async function handleDesignFormPerm(props, formInfo, elTab, formType, formId) {
let defFlowId = props.currJob.defFlowId;
let flowInstId = props.currJob.flowInstId;
let flowNodeId = props.currJob.flowNodeId;
let resp = await listFormOption({
type: DIC_PROP.FORM_DATA_TYPE[1].value, formType: formType, formId: formId,
defFlowId: defFlowId, flowInstId: flowInstId, flowNodeId: flowNodeId, isOnlyCurr: '1'
}).catch(() => {
useMessage().error("获取表单字段权限失败");
})
if (validateNull(resp.data)) return {};
// 全部只读部分可编辑
let callback = () => { handleFormPerms(null, null, formInfo.widgetList, elTab, resp.data, defFlowId, flowNodeId) }
initIsFormEdit(elTab, resp.data, defFlowId, flowNodeId)
return {columns: resp.data, callback, widgetList: formInfo.widgetList};
}
export async function handleCustomFormPerm(props, hiddenFields, disabledFields, elTab: Object) {
let defFlowId = props.currJob.defFlowId;
let flowInstId = props.currJob.flowInstId;
let flowNodeId = props.currJob.flowNodeId;
let resp = await listFormOption({
type: DIC_PROP.FORM_DATA_TYPE[1].value, formType: DIC_PROP.FORM_TYPE[1].value, formId: elTab.id,
defFlowId: defFlowId, flowInstId: flowInstId, flowNodeId: flowNodeId, isOnlyCurr: '1'
}).catch(() => {
useMessage().error("获取表单字段权限失败");
})
if (validateNull(resp.data)) return {};
// 全部只读部分可编辑
let callback = () => { handleFormPerms(hiddenFields, disabledFields, null, elTab, resp.data, defFlowId, flowNodeId) }
initIsFormEdit(elTab, resp.data, defFlowId, flowNodeId)
return {columns: resp.data, callback};
}
// 查询配置信息
export async function handleFormPrint(form, formType, formId, isOnlyCurr) {
await listPrintTemp({
flowInstId: form.flowInstId,
type: DIC_PROP.FORM_DATA_TYPE[2].value, formType: formType, formId: formId,
isOnlyCurr: isOnlyCurr
}).then(resp => {
let res = resp.data;
form.printInfo = res.printInfo
}).catch(() => {
useMessage().error("获取表单打印模板失败");
})
}
// 构建字段权限列表
export function buildFieldPerms(formFieldPerms: any[], widgetList: any[], subForm?, isField?) {
widgetList.forEach(f => {
// 处理子表单元素、分组元素
let isSubForm = f.type.indexOf(PROP_CONST.FORM_DESIGN.subForm) !== -1;
if (isSubForm || f.type.indexOf(PROP_CONST.FORM_DESIGN.group) !== -1) {
let widgetList = f.props.rule;
if (validateNull(widgetList)) return;
if (isSubForm) pushFormFieldPerms(formFieldPerms, f, null, isField)
let subForm2 = isSubForm ? f.field : subForm
buildFieldPerms(formFieldPerms, widgetList, subForm2, isField);
}
// 处理栅格元素、标签页元素、折叠面板元素
else if (f.type.indexOf(PROP_CONST.FORM_DESIGN.fcRow) !== -1 || f.type.indexOf(PROP_CONST.FORM_DESIGN.elTabs) !== -1 || f.type.indexOf(PROP_CONST.FORM_DESIGN.elCollapse) !== -1) {
let cols = f.children;
if (validateNull(cols)) return;
cols.forEach(col => {
if (validateNull(col.children)) return;
buildFieldPerms(formFieldPerms, col.children, subForm, isField);
})
}
// 处理表格表单元素
else if (f.type.indexOf(PROP_CONST.FORM_DESIGN.tableForm) !== -1) {
let cols = f.props.columns;
if (validateNull(cols)) return;
pushFormFieldPerms(formFieldPerms, f, null, isField)
cols.forEach(col => {
if (validateNull(col.rule)) return;
buildFieldPerms(formFieldPerms, col.rule, f.field, isField);
})
}
// 处理表格布局元素、卡片元素
else if (f.type.indexOf(PROP_CONST.FORM_DESIGN.fcTable) !== -1 || f.type.indexOf(PROP_CONST.FORM_DESIGN.elCard) !== -1) {
let cols = f.children;
if (validateNull(cols)) return;
buildFieldPerms(formFieldPerms, cols, subForm, isField);
}
else {
pushFormFieldPerms(formFieldPerms, f, subForm, isField)
}
})
}
function pushFormFieldPerms(formFieldPerms, f, subForm?, isField?){
let label = f.title;
/*if (f.type.indexOf(PROP_CONST.FORM_DESIGN.subForm) !== -1) {
label = "(子表单)" + label
}
if (f.type.indexOf(PROP_CONST.FORM_DESIGN.tableForm) !== -1) {
label = "(表单表格)" + label
}*/
// 排除无名称字段
if (!label) return
formFieldPerms.push({
propId: f.name,
prop: f.field,
label: label,
subForm: subForm,
propType: f.type,
__fc__: isField ? f.__fc__ : null
})
}
export function handleFormFieldPerms(data, $message, form, prefix) {
let isDesign = form.type !== DIC_PROP.FORM_TYPE[1].value;
if (isDesign) {
if (validateNull(form.formInfo)) {
data.formFieldPerms = [];
if ($message) $message.warning("当前选择的设计表单无字段信息,请先在《表单设计器》中设计")
return true;
} else {
buildFormFieldPerms(data, form.formInfo, prefix)
return true;
}
} else {
let formFieldPerms = form.formFieldPerms;
if (!validateNull(formFieldPerms)) {
// 不影响表单设计信息
data.formFieldPerms = deepClone(formFieldPerms)
handleFieldProp(data.formFieldPerms, prefix)
return true;
}
// 此处要查询接口
return false;
}
}
export function buildSysFieldsFormOption(data, props, $message) {
let formFieldPerms = deepClone(PROP_CONST.SYS_FIELDS);
handleFieldProp(formFieldPerms, null)
data.allFieldPerms = [{label: '系统字段', options: formFieldPerms}]
validateListFormOption(data, props, $message, () => {
let options = {label: '表单字段', options: data.formFieldPerms}
data.allFieldPerms.push(options)
})
}
export async function validateListFormOption(data, props, $message?, callback?) {
let form = props.currFlowForm ? props.currFlowForm : {}
let formId = props.flowData.attrs.formId;
if (validateNull(props.currFlowForm)) {
if (!formId) {
if ($message) $message.warning("流程属性【关联表单】为空,请点击左上角《流程属性》选择");
return
}
form.type = DIC_PROP.FORM_TYPE[1].value
form.id = formId
} else {
let isReturn = handleFormFieldPerms(data, $message, form, PROP_CONST.VAR_KEY_VAL.form);
if (isReturn) {
if (callback) callback()
return
}
}
// 判断流程实例独立配置
let flowInstId = validateRunFlow(props);
// 当系统表单没配置时查表数据
await listFormOption({
flowInstId: flowInstId, type: DIC_PROP.FORM_DATA_TYPE[0].value,
formType: form.type, formId: form.id
}).then(resp => {
let res = resp.data;
if (!validateNull(res)) {
data.formFieldPerms = res
handleFieldProp(data.formFieldPerms, PROP_CONST.VAR_KEY_VAL.form)
} else {
validateFormType(data, form, $message)
}
if (callback) callback()
}).catch(() => {
$message.error("获取系统表单字段权限失败");
})
}
function validateFormType(data, form, $message) {
// 判断系统表单
if (form.type === DIC_PROP.FORM_TYPE[1].value) {
data.formFieldPerms = [];
if ($message) $message.warning("当前选择的系统表单无字段信息,请先在表单设计中录入")
return;
}
buildFormFieldPerms(data, form.formInfo, PROP_CONST.VAR_KEY_VAL.form)
}
function buildFormFieldPerms(data, formInfo, prefix) {
data.formFieldPerms = []
buildFieldPerms(data.formFieldPerms, formInfo.widgetList);
handleFieldProp(data.formFieldPerms, prefix)
}
export function handleFieldProp(formFieldPerms, prefix) {
if (validateNull(formFieldPerms)) return
formFieldPerms.forEach(each => {
// 暂时未用each.prefix
each.prop = (each.prefix ? each.prefix : prefix) + (each.subForm ? each.subForm + '.' + each.prop : each.prop)
})
}
// 初始化
function initIsFormEdit(elTab, formPerms, defFlowId, flowNodeId) {
if (validateNull(formPerms)) return;
// 判断是否存在
let formFieldPerms = formPerms.filter(f => f.defFlowId === defFlowId && f.flowNodeId === flowNodeId);
if (validateNull(formFieldPerms)) return
// 判断全部只读部分可编辑
let find = formFieldPerms.find(f => f.permType === '1');
if (find && elTab.isFormEdit === '0') {
// 记录初始值
elTab.defFormEdit = elTab.isFormEdit
elTab.isFormEdit = '1'
}
}
// 处理表单字段权限
function handleFormPerms(hiddenFields, disabledFields, columns, elTab, formPerms, defFlowId, flowNodeId) {
if (validateNull(formPerms)) return;
// 判断是否存在
let formFieldPerms = formPerms.filter(f => f.defFlowId === defFlowId && f.flowNodeId === flowNodeId);
if (validateNull(formFieldPerms)) return
// 处理权限
formFieldPerms.forEach(each => {
if (validateNull(columns)) {
extractedPermType(hiddenFields, disabledFields, elTab, each, null);
} else {
doFormFieldPerms(hiddenFields, disabledFields, columns, each, elTab);
}
})
}
function doFormFieldPerms(hiddenFields, disabledFields, columns, each, elTab) {
let find = columns.find(f => each.propId ? f.name === each.propId : f.field === each.prop)
if (find) {
extractedPermType(hiddenFields, disabledFields, elTab, each, find);
return
}
columns.forEach(f => {
// 处理子表单元素、分组元素
let isSubForm = f.type.indexOf(PROP_CONST.FORM_DESIGN.subForm) !== -1;
if (isSubForm || f.type.indexOf(PROP_CONST.FORM_DESIGN.group) !== -1) {
let widgetList = f.props.rule;
if (validateNull(widgetList)) return;
doFormFieldPerms(hiddenFields, disabledFields, widgetList, each, elTab)
}
// 处理栅格元素、标签页元素、折叠面板元素
else if (f.type.indexOf(PROP_CONST.FORM_DESIGN.fcRow) !== -1 || f.type.indexOf(PROP_CONST.FORM_DESIGN.elTabs) !== -1 || f.type.indexOf(PROP_CONST.FORM_DESIGN.elCollapse) !== -1) {
let cols = f.children;
if (validateNull(cols)) return;
cols.forEach(col => {
if (validateNull(col.children)) return;
doFormFieldPerms(hiddenFields, disabledFields, col.children, each, elTab)
})
}
// 处理表格表单元素
else if (f.type.indexOf(PROP_CONST.FORM_DESIGN.tableForm) !== -1) {
let cols = f.props.columns;
if (validateNull(cols)) return;
cols.forEach(col => {
if (validateNull(col.rule)) return;
doFormFieldPerms(hiddenFields, disabledFields, col.rule, each, elTab)
})
}
// 处理表格布局元素、卡片元素
else if (f.type.indexOf(PROP_CONST.FORM_DESIGN.fcTable) !== -1 || f.type.indexOf(PROP_CONST.FORM_DESIGN.elCard) !== -1) {
let cols = f.children;
if (validateNull(cols)) return;
doFormFieldPerms(hiddenFields, disabledFields, cols, each, elTab)
}
})
}
function extractedPermType(hiddenFields, disabledFields, elTab, each, find) {
if (elTab.type === DIC_PROP.FORM_TYPE[1].value) {
let propDef = each.subForm ? each.subForm + '.' + each.prop : each.prop;
if (each.permType === '-1') {
hiddenFields[propDef] = true
} else if (each.permType === '1') {
disabledFields[propDef] = false
} else if (each.permType === '0') {
disabledFields[propDef] = true
}
} else {
if (each.permType === '-1') {
find.hidden = true
} else if (each.permType === '1') {
if (!find.props) find.props = {}
find.props.disabled = false
} else if (each.permType === '0') {
if (!find.props) find.props = {}
find.props.disabled = true
}
}
}
export function disabledAllFormFields(columns, permType) {
if (validateNull(columns)) return
columns.forEach(f => {
// 处理子表单元素、分组元素
let isSubForm = f.type.indexOf(PROP_CONST.FORM_DESIGN.subForm) !== -1;
if (isSubForm || f.type.indexOf(PROP_CONST.FORM_DESIGN.group) !== -1) {
let widgetList = f.props.rule;
if (validateNull(widgetList)) return;
if (isSubForm) disabledFormField(f, permType)
disabledAllFormFields(widgetList, permType)
}
// 处理栅格元素、标签页元素、折叠面板元素
else if (f.type.indexOf(PROP_CONST.FORM_DESIGN.fcRow) !== -1 || f.type.indexOf(PROP_CONST.FORM_DESIGN.elTabs) !== -1 || f.type.indexOf(PROP_CONST.FORM_DESIGN.elCollapse) !== -1) {
let cols = f.children;
if (validateNull(cols)) return;
cols.forEach(col => {
if (validateNull(col.children)) return;
disabledAllFormFields(col.children, permType)
})
}
// 处理表格表单元素
else if (f.type.indexOf(PROP_CONST.FORM_DESIGN.tableForm) !== -1) {
let cols = f.props.columns;
if (validateNull(cols)) return;
disabledFormField(f, permType)
cols.forEach(col => {
if (validateNull(col.rule)) return;
disabledAllFormFields(col.rule, permType)
})
}
// 处理表格布局元素、卡片元素
else if (f.type.indexOf(PROP_CONST.FORM_DESIGN.fcTable) !== -1 || f.type.indexOf(PROP_CONST.FORM_DESIGN.elCard) !== -1) {
let cols = f.children;
if (validateNull(cols)) return;
disabledAllFormFields(cols, permType)
} else {
disabledFormField(f, permType)
}
})
}
function disabledFormField(find, permType) {
if (permType === '-1') {
find.hidden = true
} else if (permType === '1') {
if (!find.props) find.props = {}
find.props.disabled = false
} else if (permType === '0') {
if (!find.props) find.props = {}
find.props.disabled = true
}
}

129
src/flow/utils/index.ts Normal file
View File

@@ -0,0 +1,129 @@
import * as orderVue from "/@/api/order/order-key-vue";
import {isMobile} from "/@/utils/other";
import {dynamicRoutesFlow} from "/@/flow/support/extend";
import {listFlowApplicationByFlowKey} from "/@/flow/designer/api/jsonflow";
import {utils} from "/@/flow/designer/utils/common";
import {setPropsNull} from "/@/flow/support/common";
import {ElMessageBox} from "element-plus";
import {i18n} from "/@/i18n";
import {validateNull} from "/@/utils/validate";
import {validateNodeType} from "/@/flow/designer/components";
import {notifyLeft} from "/@/flow";
/**
* 常用工具类
*
* @author luolin
*/
const { t } = i18n.global;
export const formWidgetDesignHeight = (browserHeight) => {
let initClientHeight = document.getElementById("initClientHeight");
let height = (browserHeight - initClientHeight.offsetTop) + "px";
initClientHeight.style.height = height;
setTimeout(() => {
let sidePanel = document.getElementsByClassName("el-aside _fc-l")[0];
if (sidePanel) sidePanel.style.height = height;
// TODO 预览时不保存默认formData
// let formWidgetMain = document.getElementsByClassName("el-container is-vertical _fc-m")[0];
// if (formWidgetMain) formWidgetMain.style.height = height;
}, 2000)
}
// 可视窗口默认无高度
export const initFlowDesignHeight = ($route, browserHeight) => {
let initClientHeight = document.getElementById("initClientHeight");
if (isMobile()) {
// 判断是否在APP中访问
if (initClientHeight.parentNode.id !== 'app') {
browserHeight = browserHeight - 187
if ($route.path !== dynamicRoutesFlow[0].path) browserHeight = browserHeight + 120
}
} else {
if ($route.path === dynamicRoutesFlow[0].path) browserHeight = browserHeight - 120
browserHeight = browserHeight - initClientHeight.offsetTop
}
initClientHeight.style.height = browserHeight + "px"
}
// 隐藏弹出框滚动条
export const initHandleJobHeight = (data) => {
let handleJob = document.getElementById('handle_job');
if (handleJob) {
handleJob.style.overflowX = 'hidden'
if (data.currElTab.path === orderVue.vueKey.FlowDesignView) {
handleJob.style.overflowY = 'hidden'
} else {
handleJob.style.overflowY = 'auto'
}
}
}
// 隐藏流程图弹出框
export const hideVueContextmenuName = () => {
let element = document.querySelector(".vue-contextmenuName-node-menu");
if (element) element.style.display = "none";
let element2 = document.querySelector(".vue-contextmenuName-link-menu");
if (element2) element2.style.display = "none";
let element3 = document.querySelector(".vue-contextmenuName-node-connect-menu");
if (element3) element3.style.display = "none";
let element4 = document.querySelector(".vue-contextmenuName-flow-menu");
if (element4) element4.style.display = "none";
}
// 确认窗体
export const confirmCancelAndClose = (confirmObj, cancelObj, msg) => {
// 无法await
ElMessageBox.confirm(msg, t('message.box.title'), {
distinguishCancelAndClose: true,
confirmButtonText: confirmObj.text,
cancelButtonText: cancelObj.text,
type: 'warning',
}).then(() => {
if (confirmObj.callback) confirmObj.callback()
}).catch((err) => {
if (err === 'cancel') {
if (cancelObj.callback) cancelObj.callback()
}
})
}
export const handleUpgradeVersion = async (props) => {
if (!props.currFlowForm) return
if (props.currFlowForm.isNew) return
props.currFlowForm.isNew = true
// TODO 只有模板才能升版本
let oldFormId = props.currFlowForm.id;
setPropsNull(props.currFlowForm, 'id', 'createUser', 'createTime')
let newFormId = utils.getId()
props.currFlowForm.id = newFormId
let resp = await listFlowApplicationByFlowKey(props.currFlowForm.flowKey);
if (validateNull(resp.data)) return
props.currFlowForm.version = resp.data[0].version + 1
replaceFlowNodeFormId(newFormId, oldFormId)
}
// 替换节点旧表单ID
export function replaceFlowNodeFormId(newFormId, oldFormId, isNotified?) {
let models = window._jfGraph.getElements();
if (validateNull(models)) return
let isExistReplace = false
models.forEach(model => {
if (!validateNodeType(model, null, true)) return
let pcTodoUrl = model.attributes.attrs.cdata.attrs.pcTodoUrl
if (validateNull(pcTodoUrl)) return;
const index = pcTodoUrl.findIndex(item => item === oldFormId);
if (index !== -1) {
model.attributes.attrs.cdata.attrs.pcTodoUrl.splice(index, 1, newFormId);
let formId = model.attributes.attrs.cdata.attrs.formId
if (formId === oldFormId) model.attributes.attrs.cdata.attrs.formId = newFormId
isExistReplace = true
}
})
if (!isNotified) return;
if (isExistReplace) {
notifyLeft('替换成功')
} else {
notifyLeft('节点中不存在该旧的页面', 'warning')
}
}