Commit be6ae75c by YunaiV

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

# Conflicts:
#	pnpm-lock.yaml
#	src/views/ai/model/model/index.vue
#	src/views/bpm/model/form/index.vue
parents 4092bcc5 192453e4
{
"name": "yudao-ui-admin-vue3",
"version": "2.4.0-snapshot",
"version": "2.4.1-snapshot",
"description": "基于vue3、vite4、element-plus、typesScript",
"author": "xingyu",
"private": false,
......@@ -73,6 +73,7 @@
"vue-i18n": "9.10.2",
"vue-router": "4.4.5",
"vue-types": "^5.1.1",
"vue3-signature": "^0.2.4",
"vuedraggable": "^4.1.0",
"web-storage-cache": "^1.1.1",
"xml-js": "^1.6.11"
......
......@@ -72,3 +72,7 @@ export const deleteModel = async (id: number) => {
export const deployModel = async (id: number) => {
return await request.post({ url: '/bpm/model/deploy?id=' + id })
}
export const cleanModel = async (id: number) => {
return await request.delete({ url: '/bpm/model/clean?id=' + id })
}
......@@ -36,6 +36,7 @@ export type ApprovalTaskInfo = {
assigneeUser: User
status: number
reason: string
signPicUrl: string
}
// 审批节点信息
......@@ -89,7 +90,7 @@ export const getProcessInstanceCopyPage = async (params: any) => {
// 获取审批详情
export const getApprovalDetail = async (params: any) => {
return await request.get({ url: 'bpm/process-instance/get-approval-detail' , params })
return await request.get({ url: 'bpm/process-instance/get-approval-detail', params })
}
// 获取表单字段权限
......
......@@ -83,5 +83,5 @@ export const reqCheck = (data: any) => {
// 通过短信重置密码
export const smsResetPassword = (data: any) => {
return request.post({ url: '/system/auth/sms-reset-password', data })
return request.post({ url: '/system/auth/reset-password', data })
}
......@@ -105,7 +105,7 @@ const editorConfig = computed((): IEditorConfig => {
},
// 超时时间,默认为 10 秒
timeout: 5 * 1000, // 5 秒
timeout: 15 * 1000, // 15 秒
// form-data fieldName,后端接口参数名称,默认值wangeditor-uploaded-image
fieldName: 'file',
......
......@@ -75,7 +75,7 @@ watch(
<template>
<ElIcon :class="prefixCls" :color="color" :size="size">
<svg v-if="isLocal" :class="getSvgClass" aria-hidden="true">
<svg v-if="isLocal" :class="getSvgClass">
<use :xlink:href="symbolId" />
</svg>
......
......@@ -40,13 +40,24 @@
<div class="handler-item-text">包容分支</div>
</div>
<div class="handler-item" @click="addNode(NodeType.DELAY_TIMER_NODE)">
<!-- TODO @芋艿 需要更换一下iconfont的图标 -->
<div class="handler-item-icon copy">
<span class="iconfont icon-size icon-copy"></span>
<div class="handler-item-icon delay">
<span class="iconfont icon-size icon-delay"></span>
</div>
<div class="handler-item-text">延迟器</div>
</div>
</div>
<div class="handler-item" @click="addNode(NodeType.ROUTER_BRANCH_NODE)">
<div class="handler-item-icon router">
<span class="iconfont icon-size icon-router"></span>
</div>
<div class="handler-item-text">路由分支</div>
</div>
<div class="handler-item" @click="addNode(NodeType.TRIGGER_NODE)">
<div class="handler-item-icon trigger">
<span class="iconfont icon-size icon-trigger"></span>
</div>
<div class="handler-item-text">触发器</div>
</div>
</div>
<template #reference>
<div class="add-icon"><Icon icon="ep:plus" /></div>
</template>
......@@ -60,12 +71,14 @@ import {
ApproveMethodType,
AssignEmptyHandlerType,
AssignStartUserHandlerType,
ConditionType,
NODE_DEFAULT_NAME,
NodeType,
RejectHandlerType,
SimpleFlowNode
SimpleFlowNode,
DEFAULT_CONDITION_GROUP_VALUE
} from './consts'
import { generateUUID } from '@/utils'
import {generateUUID} from '@/utils'
defineOptions({
name: 'NodeHandler'
......@@ -120,7 +133,16 @@ const addNode = (type: number) => {
type: AssignEmptyHandlerType.APPROVE
},
assignStartUserHandlerType: AssignStartUserHandlerType.START_USER_AUDIT,
childNode: props.childNode
childNode: props.childNode,
taskCreateListener: {
enable: false
},
taskAssignListener: {
enable: false
},
taskCompleteListener: {
enable: false
}
}
emits('update:childNode', data)
}
......@@ -147,8 +169,11 @@ const addNode = (type: number) => {
showText: '',
type: NodeType.CONDITION_NODE,
childNode: undefined,
conditionType: 1,
defaultFlow: false
conditionSetting: {
defaultFlow: false,
conditionType: ConditionType.RULE,
conditionGroups: DEFAULT_CONDITION_GROUP_VALUE
}
},
{
id: 'Flow_' + generateUUID(),
......@@ -156,8 +181,9 @@ const addNode = (type: number) => {
showText: '未满足其它条件时,将进入此分支',
type: NodeType.CONDITION_NODE,
childNode: undefined,
conditionType: undefined,
defaultFlow: true
conditionSetting: {
defaultFlow: true
}
}
]
}
......@@ -201,7 +227,11 @@ const addNode = (type: number) => {
showText: '',
type: NodeType.CONDITION_NODE,
childNode: undefined,
defaultFlow: false
conditionSetting: {
defaultFlow: false,
conditionType: ConditionType.RULE,
conditionGroups: DEFAULT_CONDITION_GROUP_VALUE
}
},
{
id: 'Flow_' + generateUUID(),
......@@ -209,7 +239,9 @@ const addNode = (type: number) => {
showText: '未满足其它条件时,将进入此分支',
type: NodeType.CONDITION_NODE,
childNode: undefined,
defaultFlow: true
conditionSetting: {
defaultFlow: true
}
}
]
}
......@@ -225,6 +257,26 @@ const addNode = (type: number) => {
}
emits('update:childNode', data)
}
if (type === NodeType.ROUTER_BRANCH_NODE) {
const data: SimpleFlowNode = {
id: 'GateWay_' + generateUUID(),
name: NODE_DEFAULT_NAME.get(NodeType.ROUTER_BRANCH_NODE) as string,
showText: '',
type: NodeType.ROUTER_BRANCH_NODE,
childNode: props.childNode
}
emits('update:childNode', data)
}
if (type === NodeType.TRIGGER_NODE) {
const data: SimpleFlowNode = {
id: 'Activity_' + generateUUID(),
name: NODE_DEFAULT_NAME.get(NodeType.TRIGGER_NODE) as string,
showText: '',
type: NodeType.TRIGGER_NODE,
childNode: props.childNode
}
emits('update:childNode', data)
}
}
</script>
......
......@@ -44,6 +44,18 @@
:flow-node="currentNode"
@update:flow-node="handleModelValueUpdate"
/>
<!-- 路由分支节点 -->
<RouterNode
v-if="currentNode && currentNode.type === NodeType.ROUTER_BRANCH_NODE"
:flow-node="currentNode"
@update:flow-node="handleModelValueUpdate"
/>
<!-- 触发器节点 -->
<TriggerNode
v-if="currentNode && currentNode.type === NodeType.TRIGGER_NODE"
:flow-node="currentNode"
@update:flow-node="handleModelValueUpdate"
/>
<!-- 递归显示孩子节点 -->
<ProcessNodeTree
v-if="currentNode && currentNode.childNode"
......@@ -67,6 +79,8 @@ import ExclusiveNode from './nodes/ExclusiveNode.vue'
import ParallelNode from './nodes/ParallelNode.vue'
import InclusiveNode from './nodes/InclusiveNode.vue'
import DelayTimerNode from './nodes/DelayTimerNode.vue'
import RouterNode from './nodes/RouterNode.vue'
import TriggerNode from './nodes/TriggerNode.vue'
import { SimpleFlowNode, NodeType } from './consts'
import { useWatchNode } from './node'
defineOptions({
......
......@@ -25,7 +25,6 @@
<script setup lang="ts">
import SimpleProcessModel from './SimpleProcessModel.vue'
import { updateBpmSimpleModel, getBpmSimpleModel } from '@/api/bpm/simple'
import { SimpleFlowNode, NodeType, NodeId, NODE_DEFAULT_TEXT } from './consts'
import { getModel } from '@/api/bpm/model'
import { getForm, FormVO } from '@/api/bpm/form'
......@@ -35,12 +34,13 @@ import * as DeptApi from '@/api/system/dept'
import * as PostApi from '@/api/system/post'
import * as UserApi from '@/api/system/user'
import * as UserGroupApi from '@/api/bpm/userGroup'
import { BpmModelFormType } from '@/utils/constants'
defineOptions({
name: 'SimpleProcessDesigner'
})
const emits = defineEmits(['success', 'init-finished']) // 保存成功事件
const emits = defineEmits(['success']) // 保存成功事件
const props = defineProps({
modelId: {
......@@ -56,16 +56,13 @@ const props = defineProps({
required: false
},
// 可发起流程的人员编号
startUserIds : {
startUserIds: {
type: Array,
required: false
},
value: {
type: [String, Object],
required: false
}
})
const processData = inject('processData') as Ref
const loading = ref(false)
const formFields = ref<string[]>([])
const formType = ref(20)
......@@ -76,9 +73,6 @@ const deptOptions = ref<DeptApi.DeptVO[]>([]) // 部门列表
const deptTreeOptions = ref()
const userGroupOptions = ref<UserGroupApi.UserGroupVO[]>([]) // 用户组列表
// 添加当前值的引用
const currentValue = ref<SimpleFlowNode | undefined>()
provide('formFields', formFields)
provide('formType', formType)
provide('roleList', roleOptions)
......@@ -88,9 +82,11 @@ provide('deptList', deptOptions)
provide('userGroupList', userGroupOptions)
provide('deptTree', deptTreeOptions)
provide('startUserIds', props.startUserIds)
provide('tasks', [])
provide('processInstance', {})
const message = useMessage() // 国际化
const processNodeTree = ref<SimpleFlowNode | undefined>()
provide('processNodeTree', processNodeTree)
const errorDialogVisible = ref(false)
let errorNodes: SimpleFlowNode[] = []
......@@ -112,70 +108,13 @@ const updateModel = () => {
}
}
// 加载流程数据
const loadProcessData = async (data: any) => {
try {
if (data) {
const parsedData = typeof data === 'string' ? JSON.parse(data) : data
processNodeTree.value = parsedData
currentValue.value = parsedData
// 确保数据加载后刷新视图
await nextTick()
if (simpleProcessModelRef.value?.refresh) {
await simpleProcessModelRef.value.refresh()
}
}
} catch (error) {
console.error('加载流程数据失败:', error)
}
}
// 监听属性变化
watch(
() => props.value,
async (newValue, oldValue) => {
if (newValue && newValue !== oldValue) {
await loadProcessData(newValue)
}
},
{ immediate: true, deep: true }
)
// 监听流程节点树变化,自动保存
watch(
() => processNodeTree.value,
async (newValue, oldValue) => {
if (newValue && oldValue && JSON.stringify(newValue) !== JSON.stringify(oldValue)) {
await saveSimpleFlowModel(newValue)
}
},
{ deep: true }
)
const saveSimpleFlowModel = async (simpleModelNode: SimpleFlowNode) => {
if (!simpleModelNode) {
return
}
// 校验节点
errorNodes = []
validateNode(simpleModelNode, errorNodes)
if (errorNodes.length > 0) {
errorDialogVisible.value = true
return
}
try {
if (props.modelId) {
// 编辑模式
const data = {
id: props.modelId,
simpleModel: simpleModelNode
}
await updateBpmSimpleModel(data)
}
// 无论是编辑还是新建模式,都更新当前值并触发事件
currentValue.value = simpleModelNode
processData.value = simpleModelNode
emits('success', simpleModelNode)
} catch (error) {
console.error('保存失败:', error)
......@@ -229,7 +168,7 @@ onMounted(async () => {
const bpmnModel = await getModel(props.modelId)
if (bpmnModel) {
formType.value = bpmnModel.formType
if (formType.value === 10) {
if (formType.value === BpmModelFormType.NORMAL && bpmnModel.formId) {
const bpmnForm = (await getForm(bpmnModel.formId)) as unknown as FormVO
formFields.value = bpmnForm?.fields
}
......@@ -246,61 +185,18 @@ onMounted(async () => {
deptTreeOptions.value = handleTree(deptOptions.value as DeptApi.DeptVO[], 'id')
// 获取用户组列表
userGroupOptions.value = await UserGroupApi.getUserGroupSimpleList()
// 加载流程数据
if (props.modelId) {
// 获取 SIMPLE 设计器模型
const result = await getBpmSimpleModel(props.modelId)
if (result) {
await loadProcessData(result)
} else {
updateModel()
}
} else if (props.value) {
await loadProcessData(props.value)
if (processData.value) {
processNodeTree.value = processData?.value
} else {
updateModel()
}
} finally {
loading.value = false
emits('init-finished')
}
})
const simpleProcessModelRef = ref()
/** 获取当前流程数据 */
const getCurrentFlowData = async () => {
try {
if (simpleProcessModelRef.value) {
const data = await simpleProcessModelRef.value.getCurrentFlowData()
if (data) {
currentValue.value = data
return data
}
}
return currentValue.value
} catch (error) {
console.error('获取流程数据失败:', error)
return currentValue.value
}
}
// 刷新方法
const refresh = async () => {
try {
if (currentValue.value) {
await loadProcessData(currentValue.value)
}
} catch (error) {
console.error('刷新失败:', error)
}
}
defineExpose({
getCurrentFlowData,
updateModel,
loadProcessData,
refresh
})
defineExpose({})
</script>
......@@ -3,6 +3,22 @@
<div class="position-absolute top-0px right-0px bg-#fff">
<el-row type="flex" justify="end">
<el-button-group key="scale-control" size="default">
<el-button v-if="!readonly" size="default" @click="exportJson">
<Icon icon="ep:download" /> 导出
</el-button>
<el-button v-if="!readonly" size="default" @click="importJson">
<Icon icon="ep:upload" />导入
</el-button>
<!-- 用于打开本地文件-->
<input
v-if="!readonly"
type="file"
id="files"
ref="refFile"
style="display: none"
accept=".json"
@change="importLocalFile"
/>
<el-button size="default" :icon="ScaleToOriginal" @click="processReZoom()" />
<el-button size="default" :plain="true" :icon="ZoomOut" @click="zoomOut()" />
<el-button size="default" class="w-80px"> {{ scaleValue }}% </el-button>
......@@ -34,6 +50,8 @@ import ProcessNodeTree from './ProcessNodeTree.vue'
import { SimpleFlowNode, NodeType, NODE_DEFAULT_TEXT } from './consts'
import { useWatchNode } from './node'
import { ZoomOut, ZoomIn, ScaleToOriginal } from '@element-plus/icons-vue'
import { isString } from '@/utils/is'
import download from '@/utils/download'
defineOptions({
name: 'SimpleProcessModel'
......@@ -52,7 +70,7 @@ const props = defineProps({
})
const emits = defineEmits<{
'save': [node: SimpleFlowNode | undefined]
save: [node: SimpleFlowNode | undefined]
}>()
const processNodeTree = useWatchNode(props)
......@@ -85,6 +103,16 @@ const processReZoom = () => {
const errorDialogVisible = ref(false)
let errorNodes: SimpleFlowNode[] = []
const saveSimpleFlowModel = async () => {
errorNodes = []
validateNode(processNodeTree.value, errorNodes)
if (errorNodes.length > 0) {
errorDialogVisible.value = true
return
}
emits('save', processNodeTree.value)
}
// 校验节点设置。 暂时以 showText 为空 未节点错误配置
const validateNode = (node: SimpleFlowNode | undefined, errorNodes: SimpleFlowNode[]) => {
if (node) {
......@@ -143,6 +171,28 @@ const getCurrentFlowData = async () => {
defineExpose({
getCurrentFlowData
})
/** 导出 JSON */
const exportJson = () => {
download.json(new Blob([JSON.stringify(processNodeTree.value)]), 'model.json')
}
/** 导入 JSON */
const refFile = ref()
const importJson = () => {
refFile.value.click()
}
const importLocalFile = () => {
const file = refFile.value.files[0]
const reader = new FileReader()
reader.readAsText(file)
reader.onload = function () {
if (isString(this.result)) {
processNodeTree.value = JSON.parse(this.result)
emits('save', processNodeTree.value)
}
}
}
</script>
<style lang="scss" scoped></style>
......@@ -29,6 +29,11 @@ export enum NodeType {
DELAY_TIMER_NODE = 14,
/**
* 触发器节点
*/
TRIGGER_NODE = 15,
/**
* 条件节点
*/
CONDITION_NODE = 50,
......@@ -44,7 +49,11 @@ export enum NodeType {
/**
* 包容分支节点 (对应包容网关)
*/
INCLUSIVE_BRANCH_NODE = 53
INCLUSIVE_BRANCH_NODE = 53,
/**
* 路由分支节点
*/
ROUTER_BRANCH_NODE = 54
}
export enum NodeId {
......@@ -93,18 +102,27 @@ export interface SimpleFlowNode {
assignEmptyHandler?: AssignEmptyHandler
// 审批节点的审批人与发起人相同时,对应的处理类型
assignStartUserHandlerType?: number
// 条件类型
conditionType?: ConditionType
// 条件表达式
conditionExpression?: string
// 条件组
conditionGroups?: ConditionGroup
// 是否默认的条件
defaultFlow?: boolean
// 创建任务监听器
taskCreateListener?: ListenerHandler
// 创建任务监听器
taskAssignListener?: ListenerHandler
// 创建任务监听器
taskCompleteListener?: ListenerHandler
// 条件设置
conditionSetting?: ConditionSetting
// 活动的状态,用于前端节点状态展示
activityStatus?: TaskStatusEnum
// 延迟设置
delaySetting?: DelaySetting
// 路由分支
routerGroups?: RouterSetting[]
defaultFlowId?: string
// 签名
signEnable?: boolean
// 审批意见
reasonRequire?: boolean
// 触发器设置
triggerSetting?: TriggerSetting
}
// 候选人策略枚举 ( 用于审批节点。抄送节点 )
export enum CandidateStrategy {
......@@ -222,6 +240,41 @@ export type AssignEmptyHandler = {
userIds?: number[]
}
/**
* 监听器的结构定义
*/
export type ListenerHandler = {
enable: boolean
path?: string
header?: HttpRequestParam[]
body?: HttpRequestParam[]
}
export type HttpRequestParam = {
key: string
type: number
value: string
}
export enum BpmHttpRequestParamTypeEnum {
/**
* 固定值
*/
FIXED_VALUE = 1,
/**
* 表单
*/
FROM_FORM = 2
}
export const BPM_HTTP_REQUEST_PARAM_TYPES = [
{
value: 1,
label: '固定值'
},
{
value: 2,
label: '表单'
}
]
// 审批拒绝类型枚举
export enum RejectHandlerType {
/**
......@@ -315,6 +368,20 @@ export enum TimeUnitType {
DAY = 3
}
/**
* 条件节点设置结构定义,用于条件节点
*/
export type ConditionSetting = {
// 条件类型
conditionType?: ConditionType
// 条件表达式
conditionExpression?: string
// 条件组
conditionGroups?: ConditionGroup
// 是否默认的条件
defaultFlow?: boolean
}
// 条件配置类型 ( 用于条件节点配置 )
export enum ConditionType {
/**
......@@ -389,8 +456,6 @@ export enum OperationButtonType {
* 条件规则结构定义
*/
export type ConditionRule = {
type: number
opName: string
opCode: string
leftSide: string
rightSide: string
......@@ -405,6 +470,24 @@ export type ConditionGroup = {
// 条件数组
conditions: Condition[]
}
/**
* 条件组默认值
*/
export const DEFAULT_CONDITION_GROUP_VALUE = {
and: true,
conditions: [
{
and: true,
rules: [
{
opCode: '==',
leftSide: '',
rightSide: ''
}
]
}
]
}
/**
* 条件结构定义
......@@ -421,6 +504,8 @@ NODE_DEFAULT_TEXT.set(NodeType.COPY_TASK_NODE, '请配置抄送人')
NODE_DEFAULT_TEXT.set(NodeType.CONDITION_NODE, '请设置条件')
NODE_DEFAULT_TEXT.set(NodeType.START_USER_NODE, '请设置发起人')
NODE_DEFAULT_TEXT.set(NodeType.DELAY_TIMER_NODE, '请设置延迟器')
NODE_DEFAULT_TEXT.set(NodeType.ROUTER_BRANCH_NODE, '请设置路由节点')
NODE_DEFAULT_TEXT.set(NodeType.TRIGGER_NODE, '请设置触发器')
export const NODE_DEFAULT_NAME = new Map<number, string>()
NODE_DEFAULT_NAME.set(NodeType.USER_TASK_NODE, '审批人')
......@@ -428,6 +513,8 @@ NODE_DEFAULT_NAME.set(NodeType.COPY_TASK_NODE, '抄送人')
NODE_DEFAULT_NAME.set(NodeType.CONDITION_NODE, '条件')
NODE_DEFAULT_NAME.set(NodeType.START_USER_NODE, '发起人')
NODE_DEFAULT_NAME.set(NodeType.DELAY_TIMER_NODE, '延迟器')
NODE_DEFAULT_NAME.set(NodeType.ROUTER_BRANCH_NODE, '路由分支')
NODE_DEFAULT_NAME.set(NodeType.TRIGGER_NODE, '触发器')
// 候选人策略。暂时不从字典中取。 后续可能调整。控制显示顺序
export const CANDIDATE_STRATEGY: DictDataVO[] = [
......@@ -460,8 +547,8 @@ export const APPROVE_METHODS: DictDataVO[] = [
]
export const CONDITION_CONFIG_TYPES: DictDataVO[] = [
{ label: '条件表达式', value: ConditionType.EXPRESSION },
{ label: '条件规则', value: ConditionType.RULE }
{ label: '条件规则', value: ConditionType.RULE },
{ label: '条件表达式', value: ConditionType.EXPRESSION }
]
// 时间单位类型
......@@ -575,7 +662,15 @@ export enum ProcessVariableEnum {
/**
* 发起用户 ID
*/
START_USER_ID = 'PROCESS_START_USER_ID'
START_USER_ID = 'PROCESS_START_USER_ID',
/**
* 发起时间
*/
START_TIME = 'PROCESS_START_TIME',
/**
* 流程定义名称
*/
PROCESS_DEFINITION_NAME = 'PROCESS_DEFINITION_NAME'
}
/**
......@@ -604,3 +699,64 @@ export const DELAY_TYPE = [
{ label: '固定时长', value: DelayTypeEnum.FIXED_TIME_DURATION },
{ label: '固定日期', value: DelayTypeEnum.FIXED_DATE_TIME }
]
/**
* 路由分支结构定义
*/
export type RouterSetting = {
nodeId: string
conditionType: ConditionType
conditionExpression: string
conditionGroups: ConditionGroup
}
// ==================== 触发器相关定义 ====================
/**
* 触发器节点结构定义
*/
export type TriggerSetting = {
type: TriggerTypeEnum
httpRequestSetting?: HttpRequestTriggerSetting
normalFormSetting?: NormalFormTriggerSetting
}
/**
* 触发器类型枚举
*/
export enum TriggerTypeEnum {
/**
* 发送 HTTP 请求触发器
*/
HTTP_REQUEST = 1,
/**
* 更新流程表单触发器
*/
UPDATE_NORMAL_FORM = 2 // TODO @jason:FORM_UPDATE?
}
/**
* HTTP 请求触发器结构定义
*/
export type HttpRequestTriggerSetting = {
// 请求 URL
url: string
// 请求头参数设置
header?: HttpRequestParam[]
// 请求体参数设置
body?: HttpRequestParam[]
// 请求响应设置
response?: Record<string, string>[]
}
/**
* 流程表单触发器配置结构定义
*/
export type NormalFormTriggerSetting = {
// 更新表单字段
updateFormFields?: Record<string, any>
}
export const TRIGGER_TYPES: DictDataVO[] = [
{ label: 'HTTP 请求', value: TriggerTypeEnum.HTTP_REQUEST },
{ label: '修改表单数据', value: TriggerTypeEnum.UPDATE_NORMAL_FORM }
]
import { cloneDeep } from 'lodash-es'
import { TaskStatusEnum } from '@/api/bpm/task'
import * as RoleApi from '@/api/system/role'
import * as DeptApi from '@/api/system/dept'
......@@ -14,9 +13,12 @@ import {
NODE_DEFAULT_NAME,
AssignStartUserHandlerType,
AssignEmptyHandlerType,
FieldPermissionType
FieldPermissionType,
HttpRequestParam,
ProcessVariableEnum
} from './consts'
import { parseFormFields } from '@/components/FormCreate/src/utils/index'
import { parseFormFields } from '@/components/FormCreate/src/utils'
export function useWatchNode(props: { flowNode: SimpleFlowNode }): Ref<SimpleFlowNode> {
const node = ref<SimpleFlowNode>(props.flowNode)
watch(
......@@ -46,9 +48,9 @@ export function useFormFieldsPermission(defaultPermission: FieldPermissionType)
// 字段权限配置. 需要有 field, title, permissioin 属性
const fieldsPermissionConfig = ref<Array<Record<string, any>>>([])
const formType = inject<Ref<number>>('formType') // 表单类型
const formType = inject<Ref<number | undefined>>('formType', ref()) // 表单类型
const formFields = inject<Ref<string[]>>('formFields') // 流程表单字段
const formFields = inject<Ref<string[]>>('formFields', ref([])) // 流程表单字段
const getNodeConfigFormFields = (nodeFormFields?: Array<Record<string, string>>) => {
nodeFormFields = toRaw(nodeFormFields)
......@@ -104,16 +106,32 @@ export function useFormFieldsPermission(defaultPermission: FieldPermissionType)
getNodeConfigFormFields
}
}
/**
* @description 获取表单的字段
* @description 获取流程表单的字段
*/
export function useFormFields() {
const formFields = inject<Ref<string[]>>('formFields') // 流程表单字段
const formFields = inject<Ref<string[]>>('formFields', ref([])) // 流程表单字段
return parseFormCreateFields(unref(formFields))
}
// TODO @芋艿:后续需要把各种类似 useFormFieldsPermission 的逻辑,抽成一个通用方法。
/**
* @description 获取流程表单的字段和发起人字段
*/
export function useFormFieldsAndStartUser() {
const injectFormFields = inject<Ref<string[]>>('formFields', ref([])) // 流程表单字段
const formFields = parseFormCreateFields(unref(injectFormFields))
// 添加发起人
formFields.unshift({
field: ProcessVariableEnum.START_USER_ID,
title: '发起人',
required: true
})
return formFields
}
export type UserTaskFormType = {
//candidateParamArray: any[]
candidateStrategy: CandidateStrategy
approveMethod: ApproveMethodType
roleIds?: number[] // 角色
......@@ -136,10 +154,29 @@ export type UserTaskFormType = {
timeDuration?: number
maxRemindCount?: number
buttonsSetting: any[]
taskCreateListenerEnable?: boolean
taskCreateListenerPath?: string
taskCreateListener?: {
header: HttpRequestParam[]
body: HttpRequestParam[]
}
taskAssignListenerEnable?: boolean
taskAssignListenerPath?: string
taskAssignListener?: {
header: HttpRequestParam[]
body: HttpRequestParam[]
}
taskCompleteListenerEnable?: boolean
taskCompleteListenerPath?: string
taskCompleteListener?: {
header: HttpRequestParam[]
body: HttpRequestParam[]
}
signEnable: boolean
reasonRequire: boolean
}
export type CopyTaskFormType = {
// candidateParamArray: any[]
candidateStrategy: CandidateStrategy
roleIds?: number[] // 角色
deptIds?: number[] // 部门
......@@ -156,13 +193,13 @@ export type CopyTaskFormType = {
* @description 节点表单数据。 用于审批节点、抄送节点
*/
export function useNodeForm(nodeType: NodeType) {
const roleOptions = inject<Ref<RoleApi.RoleVO[]>>('roleList') // 角色列表
const postOptions = inject<Ref<PostApi.PostVO[]>>('postList') // 岗位列表
const userOptions = inject<Ref<UserApi.UserVO[]>>('userList') // 用户列表
const deptOptions = inject<Ref<DeptApi.DeptVO[]>>('deptList') // 部门列表
const userGroupOptions = inject<Ref<UserGroupApi.UserGroupVO[]>>('userGroupList') // 用户组列表
const deptTreeOptions = inject('deptTree') // 部门树
const formFields = inject<Ref<string[]>>('formFields') // 流程表单字段
const roleOptions = inject<Ref<RoleApi.RoleVO[]>>('roleList', ref([])) // 角色列表
const postOptions = inject<Ref<PostApi.PostVO[]>>('postList', ref([])) // 岗位列表
const userOptions = inject<Ref<UserApi.UserVO[]>>('userList', ref([])) // 用户列表
const deptOptions = inject<Ref<DeptApi.DeptVO[]>>('deptList', ref([])) // 部门列表
const userGroupOptions = inject<Ref<UserGroupApi.UserGroupVO[]>>('userGroupList', ref([])) // 用户组列表
const deptTreeOptions = inject('deptTree', ref()) // 部门树
const formFields = inject<Ref<string[]>>('formFields', ref([])) // 流程表单字段
const configForm = ref<UserTaskFormType | CopyTaskFormType>()
if (nodeType === NodeType.USER_TASK_NODE) {
configForm.value = {
......
......@@ -124,6 +124,7 @@ const saveConfig = async () => {
if (!valid) return false
const showText = getShowText()
if (!showText) return false
currentNode.value.name = nodeName.value!
currentNode.value.showText = showText
if (configForm.value.delayType === DelayTypeEnum.FIXED_TIME_DURATION) {
currentNode.value.delaySetting = {
......
<template>
<el-drawer
:append-to-body="true"
v-model="settingVisible"
:show-close="false"
:size="630"
:before-close="saveConfig"
>
<template #header>
<div class="config-header">
<input
v-if="showInput"
type="text"
class="config-editable-input"
@blur="blurEvent()"
v-mountedFocus
v-model="nodeName"
:placeholder="nodeName"
/>
<div v-else class="node-name">
{{ nodeName }} <Icon class="ml-1" icon="ep:edit-pen" :size="16" @click="clickIcon()" />
</div>
<div class="divide-line"></div>
</div>
</template>
<div>
<el-form label-position="top">
<el-card class="mb-15px" v-for="(item, index) in routerGroups" :key="index">
<template #header>
<div class="flex flex-items-center">
<el-text size="large">路由{{ index + 1 }}</el-text>
<el-select class="ml-15px" v-model="item.nodeId" style="width: 180px">
<el-option
v-for="node in nodeOptions"
:key="node.value"
:label="node.label"
:value="node.value"
/>
</el-select>
<el-button class="mla" type="danger" link @click="deleteRouterGroup(index)">
删除
</el-button>
</div>
</template>
<Condition
:ref="($event) => (conditionRef[index] = $event)"
v-model="routerGroups[index]"
/>
</el-card>
</el-form>
<el-button class="w-1/1" type="primary" :icon="Plus" @click="addRouterGroup">
新增路由分支
</el-button>
</div>
<template #footer>
<el-divider />
<div>
<el-button type="primary" @click="saveConfig">确 定</el-button>
<el-button @click="closeDrawer">取 消</el-button>
</div>
</template>
</el-drawer>
</template>
<script setup lang="ts">
import { Plus } from '@element-plus/icons-vue'
import { SimpleFlowNode, NodeType, ConditionType, RouterSetting } from '../consts'
import { useWatchNode, useDrawer, useNodeName } from '../node'
import Condition from './components/Condition.vue'
defineOptions({
name: 'RouterNodeConfig'
})
const message = useMessage() // 消息弹窗
const props = defineProps({
flowNode: {
type: Object as () => SimpleFlowNode,
required: true
}
})
const processNodeTree = inject<Ref<SimpleFlowNode>>('processNodeTree')
// 抽屉配置
const { settingVisible, closeDrawer, openDrawer } = useDrawer()
// 当前节点
const currentNode = useWatchNode(props)
// 节点名称
const { nodeName, showInput, clickIcon, blurEvent } = useNodeName(NodeType.ROUTER_BRANCH_NODE)
const routerGroups = ref<RouterSetting[]>([])
const nodeOptions = ref<any>([])
const conditionRef = ref([])
/** 保存配置 */
const saveConfig = async () => {
// 校验表单
let valid = true
for (const item of conditionRef.value) {
if (item && !(await item.validate())) {
valid = false
}
}
if (!valid) return false
const showText = getShowText()
if (!showText) return false
currentNode.value.name = nodeName.value!
currentNode.value.showText = showText
currentNode.value.routerGroups = routerGroups.value
settingVisible.value = false
return true
}
// 显示路由分支节点配置, 由父组件传过来
const showRouteNodeConfig = (node: SimpleFlowNode) => {
getRouterNode(processNodeTree?.value)
routerGroups.value = []
nodeName.value = node.name
if (node.routerGroups) {
routerGroups.value = node.routerGroups
}
}
const getShowText = () => {
if (!routerGroups.value || !Array.isArray(routerGroups.value) || routerGroups.value.length <= 0) {
message.warning('请配置路由!')
return ''
}
for (const route of routerGroups.value) {
if (!route.nodeId || !route.conditionType) {
message.warning('请完善路由配置项!')
return ''
}
if (route.conditionType === ConditionType.EXPRESSION && !route.conditionExpression) {
message.warning('请完善路由配置项!')
return ''
}
if (route.conditionType === ConditionType.RULE) {
for (const condition of route.conditionGroups.conditions) {
for (const rule of condition.rules) {
if (!rule.leftSide || !rule.rightSide) {
message.warning('请完善路由配置项!')
return ''
}
}
}
}
}
return `${routerGroups.value.length}条路由分支`
}
const addRouterGroup = () => {
routerGroups.value.push({
nodeId: '',
conditionType: ConditionType.RULE,
conditionExpression: '',
conditionGroups: {
and: true,
conditions: [
{
and: true,
rules: [
{
opCode: '==',
leftSide: '',
rightSide: ''
}
]
}
]
}
})
}
const deleteRouterGroup = (index: number) => {
routerGroups.value.splice(index, 1)
}
// 递归获取所有节点
const getRouterNode = (node) => {
// TODO 最好还需要满足以下要求
// 并行分支、包容分支内部节点不能跳转到外部节点
// 条件分支节点可以向上跳转到外部节点
while (true) {
if (!node) break
if (node.type !== NodeType.ROUTER_BRANCH_NODE && node.type !== NodeType.CONDITION_NODE) {
nodeOptions.value.push({
label: node.name,
value: node.id
})
}
if (!node.childNode || node.type === NodeType.END_EVENT_NODE) {
break
}
if (node.conditionNodes && node.conditionNodes.length) {
node.conditionNodes.forEach((item) => {
getRouterNode(item)
})
}
node = node.childNode
}
}
defineExpose({ openDrawer, showRouteNodeConfig }) // 暴露方法给父组件
</script>
<template>
<el-form ref="formRef" :model="condition" :rules="formRules" label-position="top">
<el-form-item label="配置方式" prop="conditionType">
<el-radio-group v-model="condition.conditionType" @change="changeConditionType">
<el-radio
v-for="(dict, indexConditionType) in conditionConfigTypes"
:key="indexConditionType"
:value="dict.value"
:label="dict.value"
>
{{ dict.label }}
</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item v-if="condition.conditionType === ConditionType.RULE && condition.conditionGroups" label="条件规则">
<div class="condition-group-tool">
<div class="flex items-center">
<div class="mr-4">条件组关系</div>
<el-switch
v-model="condition.conditionGroups.and"
inline-prompt
active-text="且"
inactive-text="或"
/>
</div>
</div>
<el-space direction="vertical" :spacer="condition.conditionGroups.and ? '且' : '或'">
<el-card
class="condition-group"
style="width: 530px"
v-for="(equation, cIdx) in condition.conditionGroups.conditions"
:key="cIdx"
>
<div
class="condition-group-delete"
v-if="condition.conditionGroups.conditions.length > 1"
>
<Icon
color="#0089ff"
icon="ep:circle-close-filled"
:size="18"
@click="deleteConditionGroup(condition.conditionGroups.conditions, cIdx)"
/>
</div>
<template #header>
<div class="flex items-center justify-between">
<div>条件组</div>
<div class="flex">
<div class="mr-4">规则关系</div>
<el-switch
v-model="equation.and"
inline-prompt
active-text="且"
inactive-text="或"
/>
</div>
</div>
</template>
<div class="flex pt-2" v-for="(rule, rIdx) in equation.rules" :key="rIdx">
<div class="mr-2">
<el-form-item
:prop="`conditionGroups.conditions.${cIdx}.rules.${rIdx}.leftSide`"
:rules="{
required: true,
message: '左值不能为空',
trigger: 'change'
}"
>
<el-select style="width: 160px" v-model="rule.leftSide">
<el-option
v-for="(field, fIdx) in fieldOptions"
:key="fIdx"
:label="field.title"
:value="field.field"
:disabled="!field.required"
/>
</el-select>
</el-form-item>
</div>
<div class="mr-2">
<el-select v-model="rule.opCode" style="width: 100px">
<el-option
v-for="operator in COMPARISON_OPERATORS"
:key="operator.value"
:label="operator.label"
:value="operator.value"
/>
</el-select>
</div>
<div class="mr-2">
<el-form-item
:prop="`conditionGroups.conditions.${cIdx}.rules.${rIdx}.rightSide`"
:rules="{
required: true,
message: '右值不能为空',
trigger: 'blur'
}"
>
<el-input v-model="rule.rightSide" style="width: 160px" />
</el-form-item>
</div>
<div class="mr-1 flex items-center" v-if="equation.rules.length > 1">
<Icon icon="ep:delete" :size="18" @click="deleteConditionRule(equation, rIdx)" />
</div>
<div class="flex items-center">
<Icon icon="ep:plus" :size="18" @click="addConditionRule(equation, rIdx)" />
</div>
</div>
</el-card>
</el-space>
<div title="添加条件组" class="mt-4 cursor-pointer">
<Icon
color="#0089ff"
icon="ep:plus"
:size="24"
@click="addConditionGroup(condition.conditionGroups?.conditions)"
/>
</div>
</el-form-item>
<el-form-item
v-if="condition.conditionType === ConditionType.EXPRESSION"
label="条件表达式"
prop="conditionExpression"
>
<el-input
type="textarea"
v-model="condition.conditionExpression"
clearable
style="width: 100%"
/>
</el-form-item>
</el-form>
</template>
<script setup lang="ts">
import {
COMPARISON_OPERATORS,
CONDITION_CONFIG_TYPES,
ConditionType,
DEFAULT_CONDITION_GROUP_VALUE
} from '../../consts'
import { BpmModelFormType } from '@/utils/constants'
import { useFormFieldsAndStartUser } from '../../node'
const props = defineProps({
modelValue: {
type: Object,
required: true
}
})
const emit = defineEmits(['update:modelValue'])
const condition = computed({
get() {
return props.modelValue
},
set(newValue) {
emit('update:modelValue', newValue)
}
})
const formType = inject<Ref<number>>('formType') // 表单类型
const conditionConfigTypes = computed(() => {
return CONDITION_CONFIG_TYPES.filter((item) => {
// 业务表单暂时去掉条件规则选项
if (formType?.value === BpmModelFormType.CUSTOM && item.value === ConditionType.RULE) {
return false
} else {
return true
}
})
})
/** 条件规则可选择的表单字段 */
const fieldOptions = useFormFieldsAndStartUser()
// 表单校验规则
const formRules = reactive({
conditionType: [{ required: true, message: '配置方式不能为空', trigger: 'blur' }],
conditionExpression: [{ required: true, message: '条件表达式不能为空', trigger: 'blur' }]
})
const formRef = ref() // 表单 Ref
/** 切换条件配置方式 */
const changeConditionType = () => {
if (condition.value.conditionType === ConditionType.RULE) {
if (!condition.value.conditionGroups) {
condition.value.conditionGroups = DEFAULT_CONDITION_GROUP_VALUE
}
}
}
const deleteConditionGroup = (conditions, index) => {
conditions.splice(index, 1)
}
const deleteConditionRule = (condition, index) => {
condition.rules.splice(index, 1)
}
const addConditionRule = (condition, index) => {
const rule = {
opCode: '==',
leftSide: '',
rightSide: ''
}
condition.rules.splice(index + 1, 0, rule)
}
const addConditionGroup = (conditions) => {
const condition = {
and: true,
rules: [
{
opCode: '==',
leftSide: '',
rightSide: ''
}
]
}
conditions.push(condition)
}
const validate = async () => {
if (!formRef) return false
return await formRef.value.validate()
}
defineExpose({ validate })
</script>
<style lang="scss" scoped>
.condition-group-tool {
display: flex;
justify-content: space-between;
width: 500px;
margin-bottom: 20px;
}
.condition-group {
position: relative;
&:hover {
border-color: #0089ff;
.condition-group-delete {
opacity: 1;
}
}
.condition-group-delete {
position: absolute;
top: 0;
left: 0;
display: flex;
cursor: pointer;
opacity: 0;
}
}
::v-deep(.el-card__header) {
padding: 8px var(--el-card-padding);
border-bottom: 1px solid var(--el-card-border-color);
box-sizing: border-box;
}
</style>
<template>
<el-form-item label="请求头">
<div class="flex pt-2" v-for="(item, index) in props.header" :key="index">
<div class="mr-2">
<el-form-item
:prop="`${bind}.header.${index}.key`"
:rules="{
required: true,
message: '参数名不能为空',
trigger: 'blur'
}"
>
<el-input class="w-160px" v-model="item.key" />
</el-form-item>
</div>
<div class="mr-2">
<el-select class="w-100px!" v-model="item.type">
<el-option
v-for="types in BPM_HTTP_REQUEST_PARAM_TYPES"
:key="types.value"
:label="types.label"
:value="types.value"
/>
</el-select>
</div>
<div class="mr-2">
<el-form-item
:prop="`${bind}.header.${index}.value`"
:rules="{
required: true,
message: '参数值不能为空',
trigger: 'blur'
}"
>
<el-input
v-if="item.type === BpmHttpRequestParamTypeEnum.FIXED_VALUE"
class="w-160px"
v-model="item.value"
/>
</el-form-item>
<el-form-item
:prop="`${bind}.header.${index}.value`"
:rules="{
required: true,
message: '参数值不能为空',
trigger: 'change'
}"
>
<el-select
v-if="item.type === BpmHttpRequestParamTypeEnum.FROM_FORM"
class="w-160px!"
v-model="item.value"
>
<el-option
v-for="(field, fIdx) in formFieldOptions"
:key="fIdx"
:label="field.title"
:value="field.field"
:disabled="!field.required"
/>
</el-select>
</el-form-item>
</div>
<div class="mr-1 flex items-center">
<Icon icon="ep:delete" :size="18" @click="deleteHttpRequestParam(props.header, index)" />
</div>
</div>
<el-button type="primary" text @click="addHttpRequestParam(props.header)">
<Icon icon="ep:plus" class="mr-5px" />添加一行
</el-button>
</el-form-item>
<el-form-item label="请求体">
<div class="flex pt-2" v-for="(item, index) in props.body" :key="index">
<div class="mr-2">
<el-form-item
:prop="`${bind}.body.${index}.key`"
:rules="{
required: true,
message: '参数名不能为空',
trigger: 'blur'
}"
>
<el-input class="w-160px" v-model="item.key" />
</el-form-item>
</div>
<div class="mr-2">
<el-select class="w-100px!" v-model="item.type">
<el-option
v-for="types in BPM_HTTP_REQUEST_PARAM_TYPES"
:key="types.value"
:label="types.label"
:value="types.value"
/>
</el-select>
</div>
<div class="mr-2">
<el-form-item
:prop="`${bind}.body.${index}.value`"
:rules="{
required: true,
message: '参数值不能为空',
trigger: 'blur'
}"
>
<el-input
v-if="item.type === BpmHttpRequestParamTypeEnum.FIXED_VALUE"
class="w-160px"
v-model="item.value"
/>
</el-form-item>
<el-form-item
:prop="`${bind}.body.${index}.value`"
:rules="{
required: true,
message: '参数值不能为空',
trigger: 'change'
}"
>
<el-select
v-if="item.type === BpmHttpRequestParamTypeEnum.FROM_FORM"
class="w-160px!"
v-model="item.value"
>
<el-option
v-for="(field, fIdx) in formFieldOptions"
:key="fIdx"
:label="field.title"
:value="field.field"
:disabled="!field.required"
/>
</el-select>
</el-form-item>
</div>
<div class="mr-1 flex items-center">
<Icon icon="ep:delete" :size="18" @click="deleteHttpRequestParam(props.body, index)" />
</div>
</div>
<el-button type="primary" text @click="addHttpRequestParam(props.body)">
<Icon icon="ep:plus" class="mr-5px" />添加一行
</el-button>
</el-form-item>
</template>
<script setup lang="ts">
import { HttpRequestParam, BPM_HTTP_REQUEST_PARAM_TYPES, BpmHttpRequestParamTypeEnum } from '../../consts'
import { useFormFieldsAndStartUser } from '../../node'
defineOptions({
name: 'HttpRequestParamSetting'
})
const props = defineProps({
header: {
type: Array as () => HttpRequestParam[],
required: false,
default: () => []
},
body: {
type: Array as () => HttpRequestParam[],
required: false,
default: () => []
},
bind: {
type: String,
required: true
}
})
// 流程表单字段,发起人字段
const formFieldOptions = useFormFieldsAndStartUser()
/** 添加请求配置项 */
const addHttpRequestParam = (arr: HttpRequestParam[]) => {
arr.push({
key: '',
type: BpmHttpRequestParamTypeEnum.FIXED_VALUE,
value: ''
})
}
/** 删除请求配置项 */
const deleteHttpRequestParam = (arr: HttpRequestParam[], index: number) => {
arr.splice(index, 1)
}
</script>
<style lang="scss" scoped></style>
<template>
<el-form ref="listenerFormRef" :model="configForm" label-position="top">
<div v-for="(listener, listenerIdx) in taskListener" :key="listenerIdx">
<el-divider content-position="left">
<el-text tag="b" size="large">{{ listener.name }}</el-text>
</el-divider>
<el-form-item>
<el-switch
v-model="configForm[`task${listener.type}ListenerEnable`]"
active-text="开启"
inactive-text="关闭"
/>
</el-form-item>
<div v-if="configForm[`task${listener.type}ListenerEnable`]">
<el-form-item>
<el-alert
title="仅支持 POST 请求,以请求体方式接收参数"
type="warning"
show-icon
:closable="false"
/>
</el-form-item>
<el-form-item
label="请求地址"
:prop="`task${listener.type}ListenerPath`"
:rules="{
required: true,
message: '请求地址不能为空',
trigger: 'blur'
}"
>
<el-input v-model="configForm[`task${listener.type}ListenerPath`]" />
</el-form-item>
<HttpRequestParamSetting
:header="configForm[`task${listener.type}Listener`].header"
:body="configForm[`task${listener.type}Listener`].body"
:bind="`task${listener.type}Listener`"
/>
</div>
</div>
</el-form>
</template>
<script setup lang="ts">
import HttpRequestParamSetting from './HttpRequestParamSetting.vue'
const props = defineProps({
modelValue: {
type: Object,
required: true
},
formFieldOptions: {
type: Object,
required: true
}
})
const emit = defineEmits(['update:modelValue'])
const listenerFormRef = ref()
const configForm = computed({
get() {
return props.modelValue
},
set(newValue) {
emit('update:modelValue', newValue)
}
})
const taskListener = ref([
{
name: '创建任务',
type: 'Create'
},
{
name: '指派任务执行人员',
type: 'Assign'
},
{
name: '完成任务',
type: 'Complete'
}
])
const validate = async () => {
if (!listenerFormRef) return false
return await listenerFormRef.value.validate()
}
defineExpose({ validate })
</script>
......@@ -9,8 +9,7 @@
]"
>
<div class="node-title-container">
<!-- TODO @芋艿 需要更换图标 -->
<div class="node-title-icon copy-task"><span class="iconfont icon-copy"></span></div>
<div class="node-title-icon delay-node"><span class="iconfont icon-delay"></span></div>
<input
v-if="!readonly && showInput"
type="text"
......
......@@ -77,7 +77,7 @@ const props = defineProps({
const currentNode = useWatchNode(props)
// 是否只读
const readonly = inject<Boolean>('readonly')
const processInstance = inject<Ref<any>>('processInstance')
const processInstance = inject<Ref<any>>('processInstance', ref({}))
// 审批信息的弹窗显示,用于只读模式
const dialogVisible = ref(false) // 弹窗可见性
const processInstanceInfos = ref<any[]>([]) // 流程的审批信息
......
......@@ -108,7 +108,7 @@
<script setup lang="ts">
import NodeHandler from '../NodeHandler.vue'
import ProcessNodeTree from '../ProcessNodeTree.vue'
import { SimpleFlowNode, NodeType, NODE_DEFAULT_TEXT } from '../consts'
import { SimpleFlowNode, NodeType, ConditionType, DEFAULT_CONDITION_GROUP_VALUE, NODE_DEFAULT_TEXT } from '../consts'
import { getDefaultConditionNodeName } from '../utils'
import { useTaskStatusClass } from '../node'
import { generateUUID } from '@/utils'
......@@ -149,7 +149,7 @@ const blurEvent = (index: number) => {
showInputs.value[index] = false
const conditionNode = currentNode.value.conditionNodes?.at(index) as SimpleFlowNode
conditionNode.name =
conditionNode.name || getDefaultConditionNodeName(index, conditionNode.defaultFlow)
conditionNode.name || getDefaultConditionNodeName(index, conditionNode.conditionSetting?.defaultFlow)
}
// 点击条件名称
......@@ -178,8 +178,11 @@ const addCondition = () => {
type: NodeType.CONDITION_NODE,
childNode: undefined,
conditionNodes: [],
conditionType: 1,
defaultFlow: false
conditionSetting: {
defaultFlow: false,
conditionType: ConditionType.RULE,
conditionGroups: DEFAULT_CONDITION_GROUP_VALUE
}
}
conditionNodes.splice(lastIndex, 0, conditionData)
}
......
......@@ -34,7 +34,7 @@
]"
>
<div class="branch-node-title-container">
<div v-if="showInputs[index]">
<div v-if="!readonly && showInputs[index]">
<input
type="text"
class="editable-title-input"
......@@ -110,7 +110,7 @@
<script setup lang="ts">
import NodeHandler from '../NodeHandler.vue'
import ProcessNodeTree from '../ProcessNodeTree.vue'
import { SimpleFlowNode, NodeType, NODE_DEFAULT_TEXT } from '../consts'
import { SimpleFlowNode, NodeType, ConditionType, DEFAULT_CONDITION_GROUP_VALUE, NODE_DEFAULT_TEXT } from '../consts'
import { useTaskStatusClass } from '../node'
import { getDefaultInclusiveConditionNodeName } from '../utils'
import { generateUUID } from '@/utils'
......@@ -153,7 +153,7 @@ const blurEvent = (index: number) => {
showInputs.value[index] = false
const conditionNode = currentNode.value.conditionNodes?.at(index) as SimpleFlowNode
conditionNode.name =
conditionNode.name || getDefaultInclusiveConditionNodeName(index, conditionNode.defaultFlow)
conditionNode.name || getDefaultInclusiveConditionNodeName(index, conditionNode.conditionSetting?.defaultFlow)
}
// 点击条件名称
......@@ -182,8 +182,11 @@ const addCondition = () => {
type: NodeType.CONDITION_NODE,
childNode: undefined,
conditionNodes: [],
conditionType: 1,
defaultFlow: false
conditionSetting: {
defaultFlow: false,
conditionType: ConditionType.RULE,
conditionGroups: DEFAULT_CONDITION_GROUP_VALUE
}
}
conditionNodes.splice(lastIndex, 0, conditionData)
}
......
<template>
<div class="node-wrapper">
<div class="node-container">
<div
class="node-box"
:class="[
{ 'node-config-error': !currentNode.showText },
`${useTaskStatusClass(currentNode?.activityStatus)}`
]"
>
<div class="node-title-container">
<div class="node-title-icon router-node">
<span class="iconfont icon-router"></span>
</div>
<input
v-if="!readonly && showInput"
type="text"
class="editable-title-input"
@blur="blurEvent()"
v-mountedFocus
v-model="currentNode.name"
:placeholder="currentNode.name"
/>
<div v-else class="node-title" @click="clickTitle">
{{ currentNode.name }}
</div>
</div>
<div class="node-content" @click="openNodeConfig">
<div class="node-text" :title="currentNode.showText" v-if="currentNode.showText">
{{ currentNode.showText }}
</div>
<div class="node-text" v-else>
{{ NODE_DEFAULT_TEXT.get(NodeType.ROUTER_BRANCH_NODE) }}
</div>
<Icon v-if="!readonly" icon="ep:arrow-right-bold" />
</div>
<div v-if="!readonly" class="node-toolbar">
<div class="toolbar-icon"
><Icon color="#0089ff" icon="ep:circle-close-filled" :size="18" @click="deleteNode"
/></div>
</div>
</div>
<!-- 传递子节点给添加节点组件。会在子节点前面添加节点 -->
<NodeHandler
v-if="currentNode"
v-model:child-node="currentNode.childNode"
:current-node="currentNode"
/>
</div>
<RouterNodeConfig v-if="!readonly && currentNode" ref="nodeSetting" :flow-node="currentNode" />
</div>
</template>
<script setup lang="ts">
import { SimpleFlowNode, NodeType, NODE_DEFAULT_TEXT } from '../consts'
import NodeHandler from '../NodeHandler.vue'
import { useNodeName2, useWatchNode, useTaskStatusClass } from '../node'
import RouterNodeConfig from '../nodes-config/RouterNodeConfig.vue'
defineOptions({
name: 'RouterNode'
})
const props = defineProps({
flowNode: {
type: Object as () => SimpleFlowNode,
required: true
}
})
// 定义事件,更新父组件
const emits = defineEmits<{
'update:flowNode': [node: SimpleFlowNode | undefined]
}>()
// 是否只读
const readonly = inject<Boolean>('readonly')
// 监控节点的变化
const currentNode = useWatchNode(props)
// 节点名称编辑
const { showInput, blurEvent, clickTitle } = useNodeName2(currentNode, NodeType.ROUTER_BRANCH_NODE)
const nodeSetting = ref()
// 打开节点配置
const openNodeConfig = () => {
if (readonly) {
return
}
nodeSetting.value.showRouteNodeConfig(currentNode.value)
nodeSetting.value.openDrawer()
}
// 删除节点。更新当前节点为孩子节点
const deleteNode = () => {
emits('update:flowNode', currentNode.value.childNode)
}
</script>
<style lang="scss" scoped></style>
......@@ -13,7 +13,7 @@
><span class="iconfont icon-start-user"></span
></div>
<input
v-if="showInput"
v-if="!readonly && showInput"
type="text"
class="editable-title-input"
@blur="blurEvent()"
......@@ -117,7 +117,7 @@ const props = defineProps({
}
})
const readonly = inject<Boolean>('readonly') // 是否只读
const tasks = inject<Ref<any[]>>('tasks')
const tasks = inject<Ref<any[]>>('tasks', ref([]))
// 定义事件,更新父组件。
const emits = defineEmits<{
'update:modelValue': [node: SimpleFlowNode | undefined]
......
<template>
<div class="node-wrapper">
<div class="node-container">
<div
class="node-box"
:class="[
{ 'node-config-error': !currentNode.showText },
`${useTaskStatusClass(currentNode?.activityStatus)}`
]"
>
<div class="node-title-container">
<div class="node-title-icon trigger-node">
<span class="iconfont icon-trigger"></span>
</div>
<input
v-if="!readonly && showInput"
type="text"
class="editable-title-input"
@blur="blurEvent()"
v-mountedFocus
v-model="currentNode.name"
:placeholder="currentNode.name"
/>
<div v-else class="node-title" @click="clickTitle">
{{ currentNode.name }}
</div>
</div>
<div class="node-content" @click="openNodeConfig">
<div class="node-text" :title="currentNode.showText" v-if="currentNode.showText">
{{ currentNode.showText }}
</div>
<div class="node-text" v-else>
{{ NODE_DEFAULT_TEXT.get(NodeType.TRIGGER_NODE) }}
</div>
<Icon v-if="!readonly" icon="ep:arrow-right-bold" />
</div>
<div v-if="!readonly" class="node-toolbar">
<div class="toolbar-icon"
><Icon color="#0089ff" icon="ep:circle-close-filled" :size="18" @click="deleteNode"
/></div>
</div>
</div>
<!-- 传递子节点给添加节点组件。会在子节点前面添加节点 -->
<NodeHandler
v-if="currentNode"
v-model:child-node="currentNode.childNode"
:current-node="currentNode"
/>
</div>
<TriggerNodeConfig v-if="!readonly && currentNode" ref="nodeSetting" :flow-node="currentNode" />
</div>
</template>
<script setup lang="ts">
import { SimpleFlowNode, NodeType, NODE_DEFAULT_TEXT } from '../consts'
import NodeHandler from '../NodeHandler.vue'
import { useNodeName2, useWatchNode, useTaskStatusClass } from '../node'
import TriggerNodeConfig from '../nodes-config/TriggerNodeConfig.vue'
defineOptions({
name: 'TriggerNode'
})
const props = defineProps({
flowNode: {
type: Object as () => SimpleFlowNode,
required: true
}
})
// 定义事件,更新父组件
const emits = defineEmits<{
'update:flowNode': [node: SimpleFlowNode | undefined]
}>()
// 是否只读
const readonly = inject<Boolean>('readonly')
// 监控节点的变化
const currentNode = useWatchNode(props)
// 节点名称编辑
const { showInput, blurEvent, clickTitle } = useNodeName2(currentNode, NodeType.TRIGGER_NODE)
const nodeSetting = ref()
// 打开节点配置
const openNodeConfig = () => {
if (readonly) {
return
}
nodeSetting.value.showTriggerNodeConfig(currentNode.value)
nodeSetting.value.openDrawer()
}
// 删除节点。更新当前节点为孩子节点
const deleteNode = () => {
emits('update:flowNode', currentNode.value.childNode)
}
</script>
<style lang="scss" scoped></style>
......@@ -131,7 +131,7 @@ const emits = defineEmits<{
// 是否只读
const readonly = inject<Boolean>('readonly')
const tasks = inject<Ref<any[]>>('tasks')
const tasks = inject<Ref<any[]>>('tasks', ref([]))
// 监控节点变化
const currentNode = useWatchNode(props)
// 节点名称编辑
......
......@@ -113,18 +113,21 @@
// 节点连线气泡卡片样式
.handler-item-wrapper {
width: 320px;
display: flex;
flex-wrap: wrap;
cursor: pointer;
.handler-item {
display: flex;
flex-direction: column;
align-items: center;
margin-top: 12px;
}
.handler-item-icon {
width: 60px;
height: 60px;
width: 50px;
height: 50px;
background: #fff;
border: 1px solid #e2e2e2;
border-radius: 50%;
......@@ -138,13 +141,14 @@
.icon-size {
font-size: 25px;
line-height: 60px;
line-height: 50px;
}
}
.approve {
color: #ff943e;
}
.copy {
color: #3296fa;
}
......@@ -161,6 +165,18 @@
color: #345da2;
}
.delay {
color: #e47470;
}
.trigger {
color: #3373d2;
}
.router {
color: #ca3a31
}
.handler-item-text {
margin-top: 4px;
width: 80px;
......@@ -266,6 +282,18 @@
&.start-user {
color: #676565;
}
&.delay-node {
color: #e47470;
}
&.trigger-node {
color: #3373d2;
}
&.router-node {
color: #ca3a31
}
}
.node-title {
......@@ -711,45 +739,56 @@
// iconfont 样式
@font-face {
font-family: 'iconfont'; /* Project id 4495938 */
src:
url('iconfont.woff2?t=1724339470412') format('woff2'),
url('iconfont.woff?t=1724339470412') format('woff'),
url('iconfont.ttf?t=1724339470412') format('truetype');
font-family: "iconfont"; /* Project id 4495938 */
src: url('iconfont.woff2?t=1737639517142') format('woff2'),
url('iconfont.woff?t=1737639517142') format('woff'),
url('iconfont.ttf?t=1737639517142') format('truetype');
}
.iconfont {
font-family: 'iconfont' !important;
font-family: "iconfont" !important;
font-size: 16px;
font-style: normal;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.icon-trigger:before {
content: "\e6d3";
}
.icon-router:before {
content: "\e6b2";
}
.icon-delay:before {
content: "\e600";
}
.icon-start-user:before {
content: '\e679';
content: "\e679";
}
.icon-inclusive:before {
content: '\e602';
content: "\e602";
}
.icon-copy:before {
content: '\e7eb';
content: "\e7eb";
}
.icon-handle:before {
content: '\e61c';
content: "\e61c";
}
.icon-exclusive:before {
content: '\e717';
content: "\e717";
}
.icon-approve:before {
content: '\e715';
content: "\e715";
}
.icon-parallel:before {
content: '\e688';
content: "\e688";
}
<!-- 列表选择通用组件,参考 ProductList 组件使用 -->
<!-- TODO 芋艿:可能会移除 -->
<template>
<Dialog v-model="dialogVisible" :appendToBody="true" :scroll="true" :title="title" width="60%">
<el-table
......
......@@ -308,28 +308,6 @@ const props = defineProps({
}
})
// 监听value变化,重新加载流程图
watch(
() => props.value,
(newValue) => {
if (newValue && bpmnModeler) {
createNewDiagram(newValue)
}
},
{ immediate: true }
)
// 监听processId和processName变化
watch(
[() => props.processId, () => props.processName],
([newId, newName]) => {
if (newId && newName && !props.value) {
createNewDiagram(null)
}
},
{ immediate: true }
)
provide('configGlobal', props)
let bpmnModeler: any = null
const defaultZoom = ref(1)
......@@ -480,6 +458,7 @@ const initModelListeners = () => {
emit('commandStack-changed', event)
emit('input', xml)
emit('change', xml)
emit('save', xml)
} catch (e: any) {
console.error(`[Process Designer Warn]: ${e.message || e}`)
}
......@@ -568,6 +547,7 @@ const importLocalFile = () => {
reader.onload = function () {
let xmlStr = this.result
createNewDiagram(xmlStr)
emit('save', xmlStr)
}
}
/* ------------------------------------------------ refs methods ------------------------------------------------------ */
......
......@@ -1438,6 +1438,45 @@
"isBody": true
}
]
},
{
"name": "SignEnable",
"superClass": ["Element"],
"meta": {
"allowedIn": ["bpmn:UserTask"]
},
"properties": [
{
"name": "value",
"type": "Boolean",
"isBody": true
}
]
},
{
"name": "SkipExpression",
"extends": ["bpmn:UserTask"],
"properties": [
{
"name": "skipExpression",
"isAttr": true,
"type": "String"
}
]
},
{
"name": "ReasonRequire",
"superClass": ["Element"],
"meta": {
"allowedIn": ["bpmn:UserTask"]
},
"properties": [
{
"name": "value",
"type": "Boolean",
"isBody": true
}
]
}
],
"emumerations": []
......
<template>
<div class="process-panel__container" :style="{ width: `${width}px` }">
<div class="process-panel__container" :style="{ width: `${width}px`, maxHeight: '600px' }">
<el-collapse v-model="activeTab" v-if="isReady">
<el-collapse-item name="base">
<!-- class="panel-tab__title" -->
......
......@@ -152,6 +152,9 @@ watch(
handleKeyUpdate(props.model.key)
handleNameUpdate(props.model.name)
}
},
{
immediate: true
}
)
......
......@@ -5,6 +5,7 @@
4. 操作按钮
5. 字段权限
6. 审批类型
7. 是否需要签名
-->
<template>
<div>
......@@ -161,6 +162,16 @@
</el-radio-group>
</div>
</div>
<el-divider content-position="left">是否需要签名</el-divider>
<el-form-item prop="signEnable">
<el-switch v-model="signEnable.value" active-text="是" inactive-text="否" />
</el-form-item>
<el-divider content-position="left">审批意见</el-divider>
<el-form-item prop="reasonRequire">
<el-switch v-model="reasonRequire.value" active-text="必填" inactive-text="非必填" />
</el-form-item>
</div>
</template>
......@@ -218,6 +229,12 @@ const { formType, fieldsPermissionConfig, getNodeConfigFormFields } = useFormFie
// 审批类型
const approveType = ref({ value: ApproveType.USER })
// 是否需要签名
const signEnable = ref({ value: false })
// 审批意见
const reasonRequire = ref({ value: false })
const elExtensionElements = ref()
const otherExtensions = ref()
const bpmnElement = ref()
......@@ -311,6 +328,16 @@ const resetCustomConfigList = () => {
})
}
// 是否需要签名
signEnable.value =
elExtensionElements.value.values?.filter((ex) => ex.$type === `${prefix}:SignEnable`)?.[0] ||
bpmnInstances().moddle.create(`${prefix}:SignEnable`, { value: false })
// 审批意见
reasonRequire.value =
elExtensionElements.value.values?.filter((ex) => ex.$type === `${prefix}:ReasonRequire`)?.[0] ||
bpmnInstances().moddle.create(`${prefix}:ReasonRequire`, { value: false })
// 保留剩余扩展元素,便于后面更新该元素对应属性
otherExtensions.value =
elExtensionElements.value.values?.filter(
......@@ -322,7 +349,9 @@ const resetCustomConfigList = () => {
ex.$type !== `${prefix}:AssignEmptyUserIds` &&
ex.$type !== `${prefix}:ButtonsSetting` &&
ex.$type !== `${prefix}:FieldsPermission` &&
ex.$type !== `${prefix}:ApproveType`
ex.$type !== `${prefix}:ApproveType` &&
ex.$type !== `${prefix}:SignEnable` &&
ex.$type !== `${prefix}:ReasonRequire`
) ?? []
// 更新元素扩展属性,避免后续报错
......@@ -373,7 +402,9 @@ const updateElementExtensions = () => {
assignEmptyUserIdsEl.value,
approveType.value,
...buttonsSettingEl.value,
...fieldsPermissionEl.value
...fieldsPermissionEl.value,
signEnable.value,
reasonRequire.value
]
})
bpmnInstances().modeling.updateProperties(toRaw(bpmnElement.value), {
......
<template>
<div class="panel-tab__content">
<el-radio-group v-model="approveMethod" @change="onApproveMethodChange">
<el-radio-group
v-if="type === 'UserTask'"
v-model="approveMethod"
@change="onApproveMethodChange"
>
<div class="flex-col">
<div v-for="(item, index) in APPROVE_METHODS" :key="index">
<el-radio :value="item.value" :label="item.value">
......@@ -23,6 +27,9 @@
</div>
</div>
</el-radio-group>
<div v-else>
除了UserTask以外节点的多实例待实现
</div>
<!-- 与Simple设计器配置合并,保留以前的代码 -->
<el-form label-width="90px" style="display: none">
<el-form-item label="快捷配置">
......@@ -301,19 +308,21 @@ const approveMethod = ref()
const approveRatio = ref(100)
const otherExtensions = ref()
const getElementLoopNew = () => {
const extensionElements =
bpmnElement.value.businessObject?.extensionElements ??
bpmnInstances().moddle.create('bpmn:ExtensionElements', { values: [] })
approveMethod.value = extensionElements.values.filter(
(ex) => ex.$type === `${prefix}:ApproveMethod`
)?.[0]?.value
if (props.type === 'UserTask') {
const extensionElements =
bpmnElement.value.businessObject?.extensionElements ??
bpmnInstances().moddle.create('bpmn:ExtensionElements', { values: [] })
approveMethod.value = extensionElements.values.filter(
(ex) => ex.$type === `${prefix}:ApproveMethod`
)?.[0]?.value
otherExtensions.value =
extensionElements.values.filter((ex) => ex.$type !== `${prefix}:ApproveMethod`) ?? []
otherExtensions.value =
extensionElements.values.filter((ex) => ex.$type !== `${prefix}:ApproveMethod`) ?? []
if (!approveMethod.value) {
approveMethod.value = ApproveMethodType.SEQUENTIAL_APPROVE
updateLoopCharacteristics()
if (!approveMethod.value) {
approveMethod.value = ApproveMethodType.SEQUENTIAL_APPROVE
updateLoopCharacteristics()
}
}
}
const onApproveMethodChange = () => {
......
......@@ -192,6 +192,16 @@
<!-- 选择弹窗 -->
<ProcessExpressionDialog ref="processExpressionDialogRef" @select="selectProcessExpression" />
</el-form-item>
<el-form-item label="跳过表达式" prop="skipExpression">
<el-input
type="textarea"
v-model="userTaskForm.skipExpression"
clearable
style="width: 100%"
@change="updateSkipExpression"
/>
</el-form-item>
</el-form>
</template>
......@@ -220,7 +230,8 @@ const props = defineProps({
const prefix = inject('prefix')
const userTaskForm = ref({
candidateStrategy: undefined, // 分配规则
candidateParam: [] // 分配选项
candidateParam: [], // 分配选项
skipExpression: '' // 跳过表达式
})
const bpmnElement = ref()
const bpmnInstances = () => (window as any)?.bpmnInstances
......@@ -311,6 +322,13 @@ const resetTaskForm = () => {
(ex) => ex.$type !== `${prefix}:CandidateStrategy` && ex.$type !== `${prefix}:CandidateParam`
) ?? []
// 跳过表达式
if (businessObject.skipExpression != undefined) {
userTaskForm.value.skipExpression = businessObject.skipExpression
} else {
userTaskForm.value.skipExpression = ''
}
// 改用通过extensionElements来存储数据
return
if (businessObject.candidateStrategy != undefined) {
......@@ -390,6 +408,18 @@ const updateElementTask = () => {
})
}
const updateSkipExpression = () => {
if (userTaskForm.value.skipExpression && userTaskForm.value.skipExpression !== '') {
bpmnInstances().modeling.updateProperties(toRaw(bpmnElement.value), {
skipExpression: userTaskForm.value.skipExpression
})
} else {
bpmnInstances().modeling.updateProperties(toRaw(bpmnElement.value), {
skipExpression: null
})
}
}
// 打开监听器弹窗
const processExpressionDialogRef = ref()
const openProcessExpressionDialog = async () => {
......
import type { App } from 'vue'
import { CACHE_KEY, useCache } from '@/hooks/web/useCache'
import { useUserStore } from '@/store/modules/user'
const { t } = useI18n() // 国际化
/** 判断权限的指令 directive */
export function hasPermi(app: App<Element>) {
app.directive('hasPermi', (el, binding) => {
const { value } = binding
......@@ -19,13 +20,12 @@ export function hasPermi(app: App<Element>) {
})
}
/** 判断权限的方法 function */
const userStore = useUserStore()
const all_permission = '*:*:*'
export const hasPermission = (permission: string[]) => {
const { wsCache } = useCache()
const all_permission = '*:*:*'
const userInfo = wsCache.get(CACHE_KEY.USER)
const permissions = userInfo?.permissions || []
return permissions.some((p: string) => {
return all_permission === p || permission.includes(p)
})
}
\ No newline at end of file
return (
userStore.permissions.has(all_permission) ||
permission.some((permission) => userStore.permissions.has(permission))
)
}
......@@ -12,6 +12,10 @@ import { useDesign } from '@/hooks/web/useDesign'
import { useTemplateRefsList } from '@vueuse/core'
import { ElScrollbar } from 'element-plus'
import { useScrollTo } from '@/hooks/event/useScrollTo'
import { useTagsView } from '@/hooks/web/useTagsView'
import { cloneDeep } from 'lodash-es'
defineOptions({ name: 'TagsView' })
const { getPrefixCls } = useDesign()
......@@ -19,7 +23,9 @@ const prefixCls = getPrefixCls('tags-view')
const { t } = useI18n()
const { currentRoute, push, replace } = useRouter()
const { currentRoute, push } = useRouter()
const { closeAll, closeLeft, closeRight, closeOther, closeCurrent, refreshPage } = useTagsView()
const permissionStore = usePermissionStore()
......@@ -31,6 +37,10 @@ const visitedViews = computed(() => tagsViewStore.getVisitedViews)
const affixTagArr = ref<RouteLocationNormalizedLoaded[]>([])
const selectedTag = computed(() => tagsViewStore.getSelectedTag)
const setSelectTag = tagsViewStore.setSelectedTag
const appStore = useAppStore()
const tagsViewImmerse = computed(() => appStore.getTagsViewImmerse)
......@@ -45,82 +55,73 @@ const initTags = () => {
for (const tag of unref(affixTagArr)) {
// Must have tag name
if (tag.name) {
tagsViewStore.addVisitedView(tag)
tagsViewStore.addVisitedView(cloneDeep(tag))
}
}
}
const selectedTag = ref<RouteLocationNormalizedLoaded>()
// 新增tag
const addTags = () => {
const { name } = unref(currentRoute)
if (name) {
selectedTag.value = unref(currentRoute)
setSelectTag(unref(currentRoute))
tagsViewStore.addView(unref(currentRoute))
}
return false
}
// 关闭选中的tag
const closeSelectedTag = (view: RouteLocationNormalizedLoaded) => {
if (view?.meta?.affix) return
tagsViewStore.delView(view)
if (isActive(view)) {
toLastView()
closeCurrent(view, () => {
if (isActive(view)) {
toLastView()
}
})
}
// 去最后一个
const toLastView = () => {
const visitedViews = tagsViewStore.getVisitedViews
const latestView = visitedViews.slice(-1)[0]
if (latestView) {
push(latestView)
} else {
if (
unref(currentRoute).path === permissionStore.getAddRouters[0].path ||
unref(currentRoute).path === permissionStore.getAddRouters[0].redirect
) {
addTags()
return
}
// You can set another route
push(permissionStore.getAddRouters[0].path)
}
}
// 关闭全部
const closeAllTags = () => {
tagsViewStore.delAllViews()
toLastView()
closeAll(() => {
toLastView()
})
}
// 关闭其它
const closeOthersTags = () => {
tagsViewStore.delOthersViews(unref(selectedTag) as RouteLocationNormalizedLoaded)
closeOther()
}
// 重新加载
const refreshSelectedTag = async (view?: RouteLocationNormalizedLoaded) => {
if (!view) return
tagsViewStore.delCachedView()
const { path, query } = view
await nextTick()
replace({
path: '/redirect' + path,
query: query
})
refreshPage(view)
}
// 关闭左侧
const closeLeftTags = () => {
tagsViewStore.delLeftViews(unref(selectedTag) as RouteLocationNormalizedLoaded)
closeLeft()
}
// 关闭右侧
const closeRightTags = () => {
tagsViewStore.delRightViews(unref(selectedTag) as RouteLocationNormalizedLoaded)
}
// 跳转到最后一个
const toLastView = () => {
const visitedViews = tagsViewStore.getVisitedViews
const latestView = visitedViews.slice(-1)[0]
if (latestView) {
push(latestView)
} else {
if (
unref(currentRoute).path === permissionStore.getAddRouters[0].path ||
unref(currentRoute).path === permissionStore.getAddRouters[0].redirect
) {
addTags()
return
}
// TODO: You can set another route
push('/')
}
closeRight()
}
// 滚动到选中的tag
......@@ -209,13 +210,14 @@ const isActive = (route: RouteLocationNormalizedLoaded): boolean => {
// 所有右键菜单组件的元素
const itemRefs = useTemplateRefsList<ComponentRef<typeof ContextMenu & ContextMenuExpose>>()
// 右键菜单装填改变的时候
// 右键菜单状态改变的时候
const visibleChange = (visible: boolean, tagItem: RouteLocationNormalizedLoaded) => {
if (visible) {
for (const v of unref(itemRefs)) {
const elDropdownMenuRef = v.elDropdownMenuRef
if (tagItem.fullPath !== v.tagItem.fullPath) {
elDropdownMenuRef?.handleClose()
setSelectTag(tagItem)
}
}
}
......@@ -243,7 +245,17 @@ const move = (to: number) => {
start()
}
onMounted(() => {
const canShowIcon = (item: RouteLocationNormalizedLoaded) => {
if (
(item?.matched?.[1]?.meta?.icon && unref(tagsViewIcon)) ||
(item?.meta?.affix && unref(tagsViewIcon) && item?.meta?.icon)
) {
return true
}
return false
}
onBeforeMount(() => {
initTags()
addTags()
})
......
......@@ -308,6 +308,18 @@ const remainingRouter: AppRouteRecordRaw[] = [
})
},
{
path: 'process-instance/report',
component: () => import('@/views/bpm/processInstance/report/index.vue'),
name: 'BpmProcessInstanceReport',
meta: {
noCache: true,
hidden: true,
canTo: true,
title: '数据报表',
activeMenu: '/bpm/manager/model'
}
},
{
path: 'oa/leave/create',
component: () => import('@/views/bpm/oa/leave/create.vue'),
name: 'OALeaveCreate',
......@@ -344,7 +356,8 @@ const remainingRouter: AppRouteRecordRaw[] = [
}
},
{
path: 'manager/model/update/:id',
// TODO @zws:1)建议,在加一个路由。然后标题是“复制流程”,这样体验会好点;2)复制出来的数据,在名字前面,加“副本 ”,和钉钉保持一致!
path: 'manager/model/:type/:id',
component: () => import('@/views/bpm/model/form/index.vue'),
name: 'BpmModelUpdate',
meta: {
......
......@@ -4,16 +4,19 @@ import { getRawRoute } from '@/utils/routerHelper'
import { defineStore } from 'pinia'
import { store } from '../index'
import { findIndex } from '@/utils'
import { useUserStoreWithOut } from './user'
export interface TagsViewState {
visitedViews: RouteLocationNormalizedLoaded[]
cachedViews: Set<string>
selectedTag?: RouteLocationNormalizedLoaded
}
export const useTagsViewStore = defineStore('tagsView', {
state: (): TagsViewState => ({
visitedViews: [],
cachedViews: new Set()
cachedViews: new Set(),
selectedTag: undefined
}),
getters: {
getVisitedViews(): RouteLocationNormalizedLoaded[] {
......@@ -21,6 +24,9 @@ export const useTagsViewStore = defineStore('tagsView', {
},
getCachedViews(): string[] {
return Array.from(this.cachedViews)
},
getSelectedTag(): RouteLocationNormalizedLoaded | undefined {
return this.selectedTag
}
},
actions: {
......@@ -98,8 +104,12 @@ export const useTagsViewStore = defineStore('tagsView', {
},
// 删除所有tag
delAllVisitedViews() {
const userStore = useUserStoreWithOut()
// const affixTags = this.visitedViews.filter((tag) => tag.meta.affix)
this.visitedViews = []
this.visitedViews = userStore.getUser
? this.visitedViews.filter((tag) => tag?.meta?.affix)
: []
},
// 删除其他
delOthersViews(view: RouteLocationNormalizedLoaded) {
......@@ -145,6 +155,18 @@ export const useTagsViewStore = defineStore('tagsView', {
break
}
}
},
// 设置当前选中的 tag
setSelectedTag(tag: RouteLocationNormalizedLoaded) {
this.selectedTag = tag
},
setTitle(title: string, path?: string) {
for (const v of this.visitedViews) {
if (v.path === (path ?? this.selectedTag?.path)) {
v.meta.title = title
break
}
}
}
},
persist: false
......
......@@ -15,7 +15,7 @@ interface UserVO {
interface UserInfoVO {
// USER 缓存
permissions: string[]
permissions: Set<string>
roles: string[]
isSetUser: boolean
user: UserVO
......@@ -23,7 +23,7 @@ interface UserInfoVO {
export const useUserStore = defineStore('admin-user', {
state: (): UserInfoVO => ({
permissions: [],
permissions: new Set<string>(),
roles: [],
isSetUser: false,
user: {
......@@ -34,7 +34,7 @@ export const useUserStore = defineStore('admin-user', {
}
}),
getters: {
getPermissions(): string[] {
getPermissions(): Set<string> {
return this.permissions
},
getRoles(): string[] {
......@@ -57,7 +57,7 @@ export const useUserStore = defineStore('admin-user', {
if (!userInfo) {
userInfo = await getInfo()
}
this.permissions = userInfo.permissions
this.permissions = new Set(userInfo.permissions)
this.roles = userInfo.roles
this.user = userInfo.user
this.isSetUser = true
......@@ -85,7 +85,7 @@ export const useUserStore = defineStore('admin-user', {
this.resetState()
},
resetState() {
this.permissions = []
this.permissions = new Set<string>()
this.roles = []
this.isSetUser = false
this.user = {
......
......@@ -457,3 +457,9 @@ export const BpmProcessInstanceStatus = {
REJECT: 3, // 审批不通过
CANCEL: 4 // 已取消
}
export const BpmAutoApproveType = {
NONE: 0, // 不自动通过
APPROVE_ALL: 1, // 仅审批一次,后续重复的审批节点均自动通过
APPROVE_SEQUENT: 2, // 仅针对连续审批的节点自动通过
}
......@@ -33,6 +33,10 @@ const download = {
markdown: (data: Blob, fileName: string) => {
download0(data, fileName, 'text/markdown')
},
// 下载 Json 方法
json: (data: Blob, fileName: string) => {
download0(data, fileName, 'application/json')
},
// 下载图片(允许跨域)
image: ({
url,
......@@ -65,6 +69,31 @@ const download = {
a.download = 'image.png'
a.click()
}
},
base64ToFile: (base64: any, fileName: string) => {
// 将base64按照 , 进行分割 将前缀 与后续内容分隔开
const data = base64.split(',')
// 利用正则表达式 从前缀中获取图片的类型信息(image/png、image/jpeg、image/webp等)
const type = data[0].match(/:(.*?);/)[1]
// 从图片的类型信息中 获取具体的文件格式后缀(png、jpeg、webp)
const suffix = type.split('/')[1]
// 使用atob()对base64数据进行解码 结果是一个文件数据流 以字符串的格式输出
const bstr = window.atob(data[1])
// 获取解码结果字符串的长度
let n = bstr.length
// 根据解码结果字符串的长度创建一个等长的整形数字数组
// 但在创建时 所有元素初始值都为 0
const u8arr = new Uint8Array(n)
// 将整形数组的每个元素填充为解码结果字符串对应位置字符的UTF-16 编码单元
while (n--) {
// charCodeAt():获取给定索引处字符对应的 UTF-16 代码单元
u8arr[n] = bstr.charCodeAt(n)
}
// 将File文件对象返回给方法的调用者
return new File([u8arr], `${fileName}.${suffix}`, {
type: type
})
}
}
......
......@@ -507,3 +507,18 @@ export function jsonParse(str: string) {
return ''
}
}
/**
* 截取字符串
*
* @param name
* @param start
* @param end
*/
export const sliceName = (name: string,start: number, end : number) => {
if (name.length > end) {
return name.slice(start, end)
}
return name
}
import { CACHE_KEY, useCache } from '@/hooks/web/useCache'
import {hasPermission} from "@/directives/permission/hasPermi";
const { t } = useI18n() // 国际化
......@@ -7,21 +9,8 @@ const { t } = useI18n() // 国际化
* @param {Array} value 校验值
* @returns {Boolean}
*/
export function checkPermi(value: string[]) {
if (value && value instanceof Array && value.length > 0) {
const { wsCache } = useCache()
const permissionDatas = value
const all_permission = '*:*:*'
const userInfo = wsCache.get(CACHE_KEY.USER)
const permissions = userInfo?.permissions || []
const hasPermission = permissions.some((permission: string) => {
return all_permission === permission || permissionDatas.includes(permission)
})
return !!hasPermission
} else {
console.error(t('permission.hasPermission'))
return false
}
export function checkPermi(permission: string[]) {
return hasPermission(permission)
}
/**
......
......@@ -13,6 +13,9 @@
<el-form-item label="分类标志" prop="code">
<el-input v-model="formData.code" placeholder="请输入分类标志" />
</el-form-item>
<el-form-item label="分类描述" prop="description">
<el-input v-model="formData.description" type="textarea" placeholder="请输入分类描述" />
</el-form-item>
<el-form-item label="分类状态" prop="status">
<el-radio-group v-model="formData.status">
<el-radio
......@@ -58,6 +61,7 @@ const formData = ref({
id: undefined,
name: undefined,
code: undefined,
description: undefined,
status: CommonStatusEnum.ENABLE,
sort: undefined
})
......@@ -117,6 +121,7 @@ const resetForm = () => {
id: undefined,
name: undefined,
code: undefined,
description: undefined,
status: CommonStatusEnum.ENABLE,
sort: undefined
}
......
......@@ -50,11 +50,13 @@ import FcDesigner from '@form-create/designer'
import { encodeConf, encodeFields, setConfAndFields } from '@/utils/formCreate'
import { useTagsViewStore } from '@/store/modules/tagsView'
import { useFormCreateDesigner } from '@/components/FormCreate'
import { useRoute } from 'vue-router'
defineOptions({ name: 'BpmFormEditor' })
const { t } = useI18n() // 国际化
const message = useMessage() // 消息
const route = useRoute() // 路由
const { push, currentRoute } = useRouter() // 路由
const { query } = useRoute() // 路由信息
const { delView } = useTagsViewStore() // 视图操作
......@@ -150,6 +152,14 @@ onMounted(async () => {
const data = await FormApi.getForm(id)
formData.value = data
setConfAndFields(designer, data.conf, data.fields)
if (route.query.type !== 'copy') {
return
}
// 场景三: 复制表单
const { id: foo, ...copied } = data
formData.value = copied
formData.value.name += '_copy'
})
</script>
......
......@@ -59,7 +59,15 @@
v-hasPermi="['bpm:form:update']"
link
type="primary"
@click="openForm(scope.row.id)"
@click="openForm('copy', scope.row.id)"
>
复制
</el-button>
<el-button
v-hasPermi="['bpm:form:update']"
link
type="primary"
@click="openForm('update', scope.row.id)"
>
编辑
</el-button>
......@@ -139,16 +147,17 @@ const resetQuery = () => {
}
/** 添加/修改操作 */
const openForm = (id?: number) => {
const toRouter: { name: string; query?: { id: number } } = {
name: 'BpmFormEditor'
const openForm = (type: string, id?: number) => {
const toRouter: { name: string; query: { type: string; id?: number } } = {
name: 'BpmFormEditor',
query: {
type
}
}
console.log(typeof id)
// 表单新建的时候id传的是event需要排除
if (typeof id === 'number' || typeof id === 'string') {
toRouter.query = {
id
}
toRouter.query.id = id
}
push(toRouter)
}
......
......@@ -12,10 +12,12 @@
:additionalModel="controlForm.additionalModel"
:model="model"
@save="save"
:process-id="modelKey"
:process-name="modelName"
/>
<!-- 流程属性器,负责编辑每个流程节点的属性 -->
<MyProcessPenal
v-if="isModelerReady && modeler"
v-if="modeler"
key="penal"
:bpmnModeler="modeler"
:prefix="controlForm.prefix"
......@@ -37,8 +39,8 @@ defineOptions({ name: 'BpmModelEditor' })
const props = defineProps<{
modelId?: string
modelKey?: string
modelName?: string
modelKey: string
modelName: string
value?: string
}>()
......@@ -51,10 +53,13 @@ const formType = ref(20)
provide('formFields', formFields)
provide('formType', formType)
const xmlString = ref<string>('') // BPMN XML
// 注入流程数据
const xmlString = inject('processData') as Ref
// 注入模型数据
const modelData = inject('modelData') as Ref
const modeler = shallowRef() // BPMN Modeler
const processDesigner = ref()
const isModelerReady = ref(false)
const controlForm = ref({
simulation: true,
labelEditing: false,
......@@ -65,154 +70,26 @@ const controlForm = ref({
})
const model = ref<ModelApi.ModelVO>() // 流程模型的信息
// 初始化 bpmnInstances
const initBpmnInstances = () => {
if (!modeler.value) return false
try {
const instances = {
modeler: modeler.value,
modeling: modeler.value.get('modeling'),
moddle: modeler.value.get('moddle'),
eventBus: modeler.value.get('eventBus'),
bpmnFactory: modeler.value.get('bpmnFactory'),
elementFactory: modeler.value.get('elementFactory'),
elementRegistry: modeler.value.get('elementRegistry'),
replace: modeler.value.get('replace'),
selection: modeler.value.get('selection')
}
// 检查所有实例是否都存在
return Object.values(instances).every((instance) => instance)
} catch (error) {
console.error('初始化 bpmnInstances 失败:', error)
return false
}
}
/** 初始化 modeler */
const initModeler = async (item) => {
try {
modeler.value = item
// 等待 modeler 初始化完成
await nextTick()
// 确保 modeler 的所有实例都已经准备好
if (initBpmnInstances()) {
isModelerReady.value = true
emit('init-finished')
// 初始化完成后,设置初始值
if (props.modelId) {
// 编辑模式
const data = await ModelApi.getModel(props.modelId)
model.value = {
...data,
bpmnXml: undefined // 清空 bpmnXml 属性
}
xmlString.value = data.bpmnXml || getDefaultBpmnXml(data.key, data.name)
} else if (props.modelKey && props.modelName) {
// 新建模式
xmlString.value = props.value || getDefaultBpmnXml(props.modelKey, props.modelName)
model.value = {
key: props.modelKey,
name: props.modelName
} as ModelApi.ModelVO
}
// 导入XML并刷新视图
await nextTick()
try {
await modeler.value.importXML(xmlString.value)
if (processDesigner.value?.refresh) {
processDesigner.value.refresh()
}
} catch (error) {
console.error('导入XML失败:', error)
}
} else {
console.error('modeler 实例未完全初始化')
}
} catch (error) {
console.error('初始化 modeler 失败:', error)
}
}
/** 获取默认的BPMN XML */
const getDefaultBpmnXml = (key: string, name: string) => {
return `<?xml version="1.0" encoding="UTF-8"?>
<definitions xmlns="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:xsd="http://www.w3.org/2001/XMLSchema" targetNamespace="http://www.activiti.org/processdef">
<process id="${key}" name="${name}" isExecutable="true" />
<bpmndi:BPMNDiagram id="BPMNDiagram">
<bpmndi:BPMNPlane id="${key}_di" bpmnElement="${key}" />
</bpmndi:BPMNDiagram>
</definitions>`
const initModeler = async (item: any) => {
//先初始化模型数据
model.value = modelData.value
modeler.value = item
}
/** 添加/修改模型 */
const save = async (bpmnXml: string) => {
try {
xmlString.value = bpmnXml
if (props.modelId) {
// 编辑模式
const data = {
...model.value,
bpmnXml: bpmnXml
} as unknown as ModelApi.ModelVO
await ModelApi.updateModelBpmn(data)
emit('success')
} else {
// 新建模式,直接返回XML
emit('success', bpmnXml)
}
emit('success', bpmnXml)
} catch (error) {
console.error('保存失败:', error)
message.error('保存失败')
}
}
// 监听 key、name 和 value 的变化
watch(
[() => props.modelKey, () => props.modelName, () => props.value],
async ([newKey, newName, newValue]) => {
if (!props.modelId && isModelerReady.value) {
let shouldRefresh = false
if (newKey && newName) {
const newXml = newValue || getDefaultBpmnXml(newKey, newName)
if (newXml !== xmlString.value) {
xmlString.value = newXml
shouldRefresh = true
}
model.value = {
...model.value,
key: newKey,
name: newName
} as ModelApi.ModelVO
} else if (newValue && newValue !== xmlString.value) {
xmlString.value = newValue
shouldRefresh = true
}
if (shouldRefresh) {
// 确保更新后重新渲染
await nextTick()
if (processDesigner.value?.refresh) {
try {
await modeler.value?.importXML(xmlString.value)
processDesigner.value.refresh()
} catch (error) {
console.error('导入XML失败:', error)
}
}
}
}
},
{ deep: true }
)
// 在组件卸载时清理
onBeforeUnmount(() => {
isModelerReady.value = false
modeler.value = null
// 清理全局实例
const w = window as any
......@@ -220,55 +97,6 @@ onBeforeUnmount(() => {
w.bpmnInstances = null
}
})
/** 获取 XML 字符串 */
const saveXML = async () => {
if (!modeler.value) {
return { xml: xmlString.value }
}
try {
const result = await modeler.value.saveXML({ format: true })
xmlString.value = result.xml
return result
} catch (error) {
console.error('获取XML失败:', error)
return { xml: xmlString.value }
}
}
/** 获取SVG字符串 */
const saveSVG = async () => {
if (!modeler.value) {
return { svg: undefined }
}
try {
return await modeler.value.saveSVG()
} catch (error) {
console.error('获取SVG失败:', error)
return { svg: undefined }
}
}
/** 刷新视图 */
const refresh = async () => {
if (processDesigner.value?.refresh && modeler.value) {
try {
await modeler.value.importXML(xmlString.value)
processDesigner.value.refresh()
} catch (error) {
console.error('刷新视图失败:', error)
}
}
}
// 暴露必要的属性和方法给父组件
defineExpose({
modeler,
isModelerReady,
saveXML,
saveSVG,
refresh
})
</script>
<style lang="scss">
.process-panel__container {
......
......@@ -6,7 +6,7 @@
class="!w-440px"
v-model="modelData.key"
:disabled="!!modelData.id"
placeholder="请输入流标标识"
placeholder="请输入流程标识,以字母或下划线开头"
/>
<el-tooltip
class="item"
......@@ -41,7 +41,7 @@
/>
</el-select>
</el-form-item>
<el-form-item label="流程图标" prop="icon" class="mb-20px">
<el-form-item label="流程图标" class="mb-20px">
<UploadImg v-model="modelData.icon" :limit="1" height="64px" width="64px" />
</el-form-item>
<el-form-item label="流程描述" prop="description" class="mb-20px">
......@@ -62,7 +62,7 @@
<el-radio-group v-model="modelData.visible">
<el-radio
v-for="dict in getBoolDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING)"
:key="dict.value"
:key="dict.value as string"
:value="dict.value"
>
{{ dict.label }}
......@@ -77,7 +77,6 @@
>
<el-option label="全员" :value="0" />
<el-option label="指定人员" :value="1" />
<el-option label="均不可提交" :value="2" />
</el-select>
<div v-if="modelData.startUserType === 1" class="mt-2 flex flex-wrap gap-2">
<div
......@@ -97,21 +96,12 @@
/>
</div>
<el-button type="primary" link @click="openStartUserSelect">
<Icon icon="ep:plus" />选择人员
<Icon icon="ep:plus" /> 选择人员
</el-button>
</div>
</el-form-item>
<el-form-item label="流程管理员" prop="managerUserType" class="mb-20px">
<el-select
v-model="modelData.managerUserType"
placeholder="请选择流程管理员"
@change="handleManagerUserTypeChange"
>
<el-option label="全员" :value="0" />
<el-option label="指定人员" :value="1" />
<el-option label="均不可提交" :value="2" />
</el-select>
<div v-if="modelData.managerUserType === 1" class="mt-2 flex flex-wrap gap-2">
<el-form-item label="流程管理员" prop="managerUserIds" class="mb-20px">
<div class="flex flex-wrap gap-2">
<div
v-for="user in selectedManagerUsers"
:key="user.id"
......@@ -142,14 +132,11 @@
<script lang="ts" setup>
import { DICT_TYPE, getBoolDictOptions, getIntDictOptions } from '@/utils/dict'
import { UserVO } from '@/api/system/user'
import { CategoryVO } from '@/api/bpm/category'
const props = defineProps({
modelValue: {
type: Object,
required: true
},
categoryList: {
type: Array,
type: Array as PropType<CategoryVO[]>,
required: true
},
userList: {
......@@ -158,8 +145,6 @@ const props = defineProps({
}
})
const emit = defineEmits(['update:modelValue'])
const formRef = ref()
const selectedStartUsers = ref<UserVO[]>([])
const selectedManagerUsers = ref<UserVO[]>([])
......@@ -170,34 +155,36 @@ const rules = {
name: [{ required: true, message: '流程名称不能为空', trigger: 'blur' }],
key: [{ required: true, message: '流程标识不能为空', trigger: 'blur' }],
category: [{ required: true, message: '流程分类不能为空', trigger: 'blur' }],
icon: [{ required: true, message: '流程图标不能为空', trigger: 'blur' }],
type: [{ required: true, message: '是否可见不能为空', trigger: 'blur' }],
visible: [{ required: true, message: '是否可见不能为空', trigger: 'blur' }],
managerUserIds: [{ required: true, message: '流程管理员不能为空', trigger: 'blur' }]
}
// 创建本地数据副本
const modelData = computed({
get: () => props.modelValue,
set: (val) => emit('update:modelValue', val)
})
const modelData = defineModel<any>()
// 初始化选中的用户
watch(
() => props.modelValue,
() => modelData.value,
(newVal) => {
if (newVal.startUserIds?.length) {
selectedStartUsers.value = props.userList.filter((user: UserVO) =>
newVal.startUserIds.includes(user.id)
) as UserVO[]
} else {
selectedStartUsers.value = []
}
if (newVal.managerUserIds?.length) {
selectedManagerUsers.value = props.userList.filter((user: UserVO) =>
newVal.managerUserIds.includes(user.id)
) as UserVO[]
} else {
selectedManagerUsers.value = []
}
},
{ immediate: true }
{
immediate: true
}
)
/** 打开发起人选择 */
......@@ -215,58 +202,42 @@ const openManagerUserSelect = () => {
/** 处理用户选择确认 */
const handleUserSelectConfirm = (_, users: UserVO[]) => {
if (currentSelectType.value === 'start') {
selectedStartUsers.value = users
emit('update:modelValue', {
modelData.value = {
...modelData.value,
startUserIds: users.map((u) => u.id)
})
}
} else {
selectedManagerUsers.value = users
emit('update:modelValue', {
modelData.value = {
...modelData.value,
managerUserIds: users.map((u) => u.id)
})
}
}
}
/** 处理发起人类型变化 */
const handleStartUserTypeChange = (value: number) => {
if (value !== 1) {
selectedStartUsers.value = []
emit('update:modelValue', {
modelData.value = {
...modelData.value,
startUserIds: []
})
}
}
/** 处理管理员类型变化 */
const handleManagerUserTypeChange = (value: number) => {
if (value !== 1) {
selectedManagerUsers.value = []
emit('update:modelValue', {
...modelData.value,
managerUserIds: []
})
}
}
}
/** 移除发起人 */
const handleRemoveStartUser = (user: UserVO) => {
selectedStartUsers.value = selectedStartUsers.value.filter((u) => u.id !== user.id)
emit('update:modelValue', {
modelData.value = {
...modelData.value,
startUserIds: modelData.value.startUserIds.filter((id: number) => id !== user.id)
})
}
}
/** 移除管理员 */
const handleRemoveManagerUser = (user: UserVO) => {
selectedManagerUsers.value = selectedManagerUsers.value.filter((u) => u.id !== user.id)
emit('update:modelValue', {
modelData.value = {
...modelData.value,
managerUserIds: modelData.value.managerUserIds.filter((id: number) => id !== user.id)
})
}
}
/** 表单校验 */
......
<template>
<el-form ref="formRef" :model="modelData" label-width="120px" class="mt-20px">
<el-form-item class="mb-20px">
<template #label>
<el-text size="large" tag="b">提交人权限</el-text>
</template>
<div class="flex flex-col">
<el-checkbox v-model="modelData.allowCancelRunningProcess" label="允许撤销审批中的申请" />
<div class="ml-22px">
<el-text type="info"> 第一个审批节点通过后,提交人仍可撤销申请 </el-text>
</div>
</div>
</el-form-item>
<el-form-item v-if="modelData.processIdRule" class="mb-20px">
<template #label>
<el-text size="large" tag="b">流程编码</el-text>
</template>
<div class="flex flex-col">
<div>
<el-input
v-model="modelData.processIdRule.prefix"
class="w-130px!"
placeholder="前缀"
:disabled="!modelData.processIdRule.enable"
>
<template #prepend>
<el-checkbox v-model="modelData.processIdRule.enable" />
</template>
</el-input>
<el-select
v-model="modelData.processIdRule.infix"
class="w-130px! ml-5px"
placeholder="中缀"
:disabled="!modelData.processIdRule.enable"
>
<el-option
v-for="item in timeOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
<el-input
v-model="modelData.processIdRule.postfix"
class="w-80px! ml-5px"
placeholder="后缀"
:disabled="!modelData.processIdRule.enable"
/>
<el-input-number
v-model="modelData.processIdRule.length"
class="w-120px! ml-5px"
:min="5"
:disabled="!modelData.processIdRule.enable"
/>
</div>
<div class="ml-22px" v-if="modelData.processIdRule.enable">
<el-text type="info"> 编码示例:{{ numberExample }} </el-text>
</div>
</div>
</el-form-item>
<el-form-item class="mb-20px">
<template #label>
<el-text size="large" tag="b">自动去重</el-text>
</template>
<div class="flex flex-col">
<div>
<el-text> 同一审批人在流程中重复出现时: </el-text>
</div>
<el-radio-group v-model="modelData.autoApprovalType">
<div class="flex flex-col">
<el-radio :value="0">不自动通过</el-radio>
<el-radio :value="1">仅审批一次,后续重复的审批节点均自动通过</el-radio>
<el-radio :value="2">仅针对连续审批的节点自动通过</el-radio>
</div>
</el-radio-group>
</div>
</el-form-item>
<el-form-item v-if="modelData.titleSetting" class="mb-20px">
<template #label>
<el-text size="large" tag="b">标题设置</el-text>
</template>
<div class="flex flex-col">
<el-radio-group v-model="modelData.titleSetting.enable">
<div class="flex flex-col">
<el-radio :value="false"
>系统默认 <el-text type="info"> 展示流程名称 </el-text></el-radio
>
<el-radio :value="true">
自定义标题
<el-text>
<el-tooltip content="输入字符 '{' 即可插入表单字段" effect="light" placement="top">
<Icon icon="ep:question-filled" class="ml-5px" />
</el-tooltip>
</el-text>
</el-radio>
</div>
</el-radio-group>
<el-mention
v-if="modelData.titleSetting.enable"
v-model="modelData.titleSetting.title"
type="textarea"
prefix="{"
split="}"
whole
:options="formFieldOptions4Title"
placeholder="请插入表单字段(输入 '{' 可以选择表单字段)或输入文本"
class="w-600px!"
/>
</div>
</el-form-item>
<el-form-item
v-if="modelData.summarySetting && modelData.formType === BpmModelFormType.NORMAL"
class="mb-20px"
>
<template #label>
<el-text size="large" tag="b">摘要设置</el-text>
</template>
<div class="flex flex-col">
<el-radio-group v-model="modelData.summarySetting.enable">
<div class="flex flex-col">
<el-radio :value="false">
系统默认 <el-text type="info"> 展示表单前 3 个字段 </el-text>
</el-radio>
<el-radio :value="true"> 自定义摘要 </el-radio>
</div>
</el-radio-group>
<el-select
class="w-500px!"
v-if="modelData.summarySetting.enable"
v-model="modelData.summarySetting.summary"
multiple
placeholder="请选择要展示的表单字段"
>
<el-option
v-for="item in formFieldOptions4Summary"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</div>
</el-form-item>
</el-form>
</template>
<script setup lang="ts">
import dayjs from 'dayjs'
import { BpmAutoApproveType, BpmModelFormType } from '@/utils/constants'
import * as FormApi from '@/api/bpm/form'
import { parseFormFields } from '@/components/FormCreate/src/utils'
import { ProcessVariableEnum } from '@/components/SimpleProcessDesignerV2/src/consts'
const modelData = defineModel<any>()
/** 自定义 ID 流程编码 */
const timeOptions = ref([
{
value: '',
label: '无'
},
{
value: 'DAY',
label: '精确到日'
},
{
value: 'HOUR',
label: '精确到时'
},
{
value: 'MINUTE',
label: '精确到分'
},
{
value: 'SECOND',
label: '精确到秒'
}
])
const numberExample = computed(() => {
if (modelData.value.processIdRule.enable) {
let infix = ''
switch (modelData.value.processIdRule.infix) {
case 'DAY':
infix = dayjs().format('YYYYMMDD')
break
case 'HOUR':
infix = dayjs().format('YYYYMMDDHH')
break
case 'MINUTE':
infix = dayjs().format('YYYYMMDDHHmm')
break
case 'SECOND':
infix = dayjs().format('YYYYMMDDHHmmss')
break
default:
break
}
return (
modelData.value.processIdRule.prefix +
infix +
modelData.value.processIdRule.postfix +
'1'.padStart(modelData.value.processIdRule.length - 1, '0')
)
} else {
return ''
}
})
/** 表单选项 */
const formField = ref<Array<{ field: string; title: string }>>([])
const formFieldOptions4Title = computed(() => {
let cloneFormField = formField.value.map((item) => {
return {
label: item.title,
value: item.field
}
})
// 固定添加发起人 ID 字段
cloneFormField.unshift({
label: '流程名称',
value: ProcessVariableEnum.PROCESS_DEFINITION_NAME
})
cloneFormField.unshift({
label: '发起时间',
value: ProcessVariableEnum.START_TIME
})
cloneFormField.unshift({
label: '发起人',
value: ProcessVariableEnum.START_USER_ID
})
return cloneFormField
})
const formFieldOptions4Summary = computed(() => {
return formField.value.map((item) => {
return {
label: item.title,
value: item.field
}
})
})
/** 兼容以前未配置更多设置的流程 */
const initData = () => {
if (!modelData.value.processIdRule) {
modelData.value.processIdRule = {
enable: false,
prefix: '',
infix: '',
postfix: '',
length: 5
}
}
if (!modelData.value.autoApprovalType) {
modelData.value.autoApprovalType = BpmAutoApproveType.NONE
}
if (!modelData.value.titleSetting) {
modelData.value.titleSetting = {
enable: false,
title: ''
}
}
if (!modelData.value.summarySetting) {
modelData.value.summarySetting = {
enable: false,
summary: []
}
}
}
defineExpose({ initData })
/** 监听表单 ID 变化,加载表单数据 */
watch(
() => modelData.value.formId,
async (newFormId) => {
if (newFormId && modelData.value.formType === BpmModelFormType.NORMAL) {
const data = await FormApi.getForm(newFormId)
const result: Array<{ field: string; title: string }> = []
if (data.fields) {
data.fields.forEach((fieldStr: string) => {
parseFormFields(JSON.parse(fieldStr), result)
})
}
formField.value = result
} else {
formField.value = []
}
},
{ immediate: true }
)
</script>
......@@ -70,25 +70,16 @@ import * as FormApi from '@/api/bpm/form'
import { setConfAndFields2 } from '@/utils/formCreate'
const props = defineProps({
modelValue: {
type: Object,
required: true
},
formList: {
type: Array,
required: true
}
})
const emit = defineEmits(['update:modelValue'])
const formRef = ref()
// 创建本地数据副本
const modelData = computed({
get: () => props.modelValue,
set: (val) => emit('update:modelValue', val)
})
const modelData = defineModel<any>()
// 表单预览数据
const formPreview = ref({
......
......@@ -6,10 +6,7 @@
:model-id="modelData.id"
:model-key="modelData.key"
:model-name="modelData.name"
:value="currentBpmnXml"
ref="bpmnEditorRef"
@success="handleDesignSuccess"
@init-finished="handleEditorInit"
/>
</template>
......@@ -21,10 +18,7 @@
:model-key="modelData.key"
:model-name="modelData.name"
:start-user-ids="modelData.startUserIds"
:value="currentSimpleModel"
ref="simpleEditorRef"
@success="handleDesignSuccess"
@init-finished="handleEditorInit"
/>
</template>
</template>
......@@ -34,137 +28,16 @@ import { BpmModelType } from '@/utils/constants'
import BpmModelEditor from '../editor/index.vue'
import SimpleModelDesign from '../../simple/SimpleModelDesign.vue'
const props = defineProps({
modelValue: {
type: Object,
required: true
}
})
const emit = defineEmits(['update:modelValue', 'success'])
const bpmnEditorRef = ref()
const simpleEditorRef = ref()
const isEditorInitialized = ref(false)
// 创建本地数据副本
const modelData = computed({
get: () => props.modelValue,
set: (val) => emit('update:modelValue', val)
})
// 保存当前的流程XML或数据
const currentBpmnXml = ref('')
const currentSimpleModel = ref('')
// 初始化或更新当前的XML数据
const initOrUpdateXmlData = () => {
if (modelData.value) {
if (modelData.value.type === BpmModelType.BPMN) {
currentBpmnXml.value = modelData.value.bpmnXml || ''
} else {
currentSimpleModel.value = modelData.value.simpleModel || ''
}
}
}
// 监听modelValue的变化,更新数据
watch(
() => props.modelValue,
(newVal) => {
if (newVal) {
if (newVal.type === BpmModelType.BPMN) {
if (newVal.bpmnXml && newVal.bpmnXml !== currentBpmnXml.value) {
currentBpmnXml.value = newVal.bpmnXml
// 如果编辑器已经初始化,刷新视图
if (isEditorInitialized.value && bpmnEditorRef.value?.refresh) {
nextTick(() => {
bpmnEditorRef.value.refresh()
})
}
}
} else {
if (newVal.simpleModel && newVal.simpleModel !== currentSimpleModel.value) {
currentSimpleModel.value = newVal.simpleModel
// 如果编辑器已经初始化,刷新视图
if (isEditorInitialized.value && simpleEditorRef.value?.refresh) {
nextTick(() => {
simpleEditorRef.value.refresh()
})
}
}
}
}
},
{ immediate: true, deep: true }
)
/** 编辑器初始化完成的回调 */
const handleEditorInit = async () => {
isEditorInitialized.value = true
// 等待下一个tick,确保编辑器已经准备好
await nextTick()
const modelData = defineModel<any>()
// 初始化完成后,设置初始值
if (modelData.value.type === BpmModelType.BPMN) {
if (modelData.value.bpmnXml) {
currentBpmnXml.value = modelData.value.bpmnXml
if (bpmnEditorRef.value?.refresh) {
await nextTick()
bpmnEditorRef.value.refresh()
}
}
} else {
if (modelData.value.simpleModel) {
currentSimpleModel.value = modelData.value.simpleModel
if (simpleEditorRef.value?.refresh) {
await nextTick()
simpleEditorRef.value.refresh()
}
}
}
}
/** 获取当前流程数据 */
const getProcessData = async () => {
try {
if (modelData.value.type === BpmModelType.BPMN) {
if (!bpmnEditorRef.value || !isEditorInitialized.value) {
return currentBpmnXml.value || undefined
}
const { xml } = await bpmnEditorRef.value.saveXML()
if (xml) {
currentBpmnXml.value = xml
return xml
}
} else {
if (!simpleEditorRef.value || !isEditorInitialized.value) {
return currentSimpleModel.value || undefined
}
const flowData = await simpleEditorRef.value.getCurrentFlowData()
if (flowData) {
currentSimpleModel.value = flowData
return flowData
}
}
return modelData.value.type === BpmModelType.BPMN
? currentBpmnXml.value
: currentSimpleModel.value
} catch (error) {
console.error('获取流程数据失败:', error)
return modelData.value.type === BpmModelType.BPMN
? currentBpmnXml.value
: currentSimpleModel.value
}
}
const processData = inject('processData') as Ref
/** 表单校验 */
const validate = async () => {
try {
// 获取最新的流程数据
const processData = await getProcessData()
if (!processData) {
if (!processData.value) {
throw new Error('请设计流程')
}
return true
......@@ -172,27 +45,19 @@ const validate = async () => {
throw error
}
}
/** 处理设计器保存成功 */
const handleDesignSuccess = async (data?: any) => {
if (data) {
if (modelData.value.type === BpmModelType.BPMN) {
currentBpmnXml.value = data
} else {
currentSimpleModel.value = data
}
// 创建新的对象以触发响应式更新
const newModelData = {
...modelData.value,
bpmnXml: modelData.value.type === BpmModelType.BPMN ? data : null,
simpleModel: modelData.value.type === BpmModelType.BPMN ? null : data
}
// 使用emit更新父组件的数据
await nextTick()
emit('update:modelValue', newModelData)
emit('success', data)
//更新表单的模型数据部分
modelData.value = newModelData
}
}
......@@ -200,36 +65,7 @@ const handleDesignSuccess = async (data?: any) => {
const showDesigner = computed(() => {
return Boolean(modelData.value?.key && modelData.value?.name)
})
// 组件创建时初始化数据
onMounted(() => {
initOrUpdateXmlData()
})
// 组件卸载前保存数据
onBeforeUnmount(async () => {
try {
// 获取并保存最新的流程数据
const data = await getProcessData()
if (data) {
// 创建新的对象以触发响应式更新
const newModelData = {
...modelData.value,
bpmnXml: modelData.value.type === BpmModelType.BPMN ? data : null,
simpleModel: modelData.value.type === BpmModelType.BPMN ? null : data
}
// 使用emit更新父组件的数据
await nextTick()
emit('update:modelValue', newModelData)
}
} catch (error) {
console.error('保存数据失败:', error)
}
})
defineExpose({
validate,
getProcessData
validate
})
</script>
......@@ -85,8 +85,6 @@
</div>
</ContentWrap>
<!-- 表单弹窗:添加/修改流程 -->
<ModelForm ref="formRef" @success="getList" />
<!-- 表单弹窗:添加分类 -->
<CategoryForm ref="categoryFormRef" @success="getList" />
<!-- 弹窗:表单详情 -->
......@@ -99,7 +97,6 @@
import draggable from 'vuedraggable'
import { CategoryApi } from '@/api/bpm/category'
import * as ModelApi from '@/api/bpm/model'
import ModelForm from './ModelForm.vue'
import CategoryForm from '../category/CategoryForm.vue'
import { cloneDeep } from 'lodash-es'
import CategoryDraggableModel from './CategoryDraggableModel.vue'
......@@ -123,7 +120,6 @@ const handleQuery = () => {
}
/** 添加/修改操作 */
const formRef = ref()
const openForm = (type: string, id?: number) => {
if (type === 'create') {
push({ name: 'BpmModelCreate' })
......@@ -206,7 +202,7 @@ const getList = async () => {
}
/** 初始化 **/
onMounted(() => {
onActivated(() => {
getList()
})
</script>
......
......@@ -58,7 +58,16 @@
>
<template #default>
<div class="flex">
<el-image :src="definition.icon" class="w-32px h-32px" />
<el-image
v-if="definition.icon"
:src="definition.icon"
class="w-32px h-32px"
/>
<div v-else class="flow-icon">
<span style="font-size: 12px; color: #fff">{{
sliceName(definition.name,0,2)
}}</span>
</div>
<el-text class="!ml-10px" size="large">{{ definition.name }}</el-text>
</div>
</template>
......@@ -88,6 +97,7 @@ import * as ProcessInstanceApi from '@/api/bpm/processInstance'
import { CategoryApi, CategoryVO } from '@/api/bpm/category'
import ProcessDefinitionDetail from './ProcessDefinitionDetail.vue'
import { groupBy } from 'lodash-es'
import { sliceName } from '@/utils/index'
defineOptions({ name: 'BpmProcessInstanceCreate' })
......@@ -282,13 +292,25 @@ onMounted(() => {
</script>
<style lang="scss" scoped>
.flow-icon {
display: flex;
width: 32px;
height: 32px;
margin-right: 10px;
background-color: var(--el-color-primary);
border-radius: 0.25rem;
align-items: center;
justify-content: center;
}
.process-definition-container::before {
content: '';
border-left: 1px solid #e6e6e6;
position: absolute;
left: 20.8%;
height: 100%;
border-left: 1px solid #e6e6e6;
content: '';
}
:deep() {
.definition-item-card {
.el-card__body {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment