Skip to content
Toggle navigation
P
Projects
G
Groups
S
Snippets
Help
phsl
/
admin
This project
Loading...
Sign in
Toggle navigation
Go to a project
Project
Repository
Issues
0
Merge Requests
0
Pipelines
Wiki
Snippets
Members
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Unverified
Commit
e8e357b8
authored
Jan 17, 2025
by
芋道源码
Committed by
Gitee
Jan 17, 2025
Browse files
Options
Browse Files
Download
Plain Diff
!659 BPM:签名功能完善
Merge pull request !659 from Lesan/feature/bpm-n
parents
8e5271a6
9308eceb
Hide whitespace changes
Inline
Side-by-side
Showing
11 changed files
with
352 additions
and
296 deletions
+352
-296
src/api/bpm/processInstance/index.ts
+1
-1
src/components/SimpleProcessDesignerV2/src/nodes-config/RouterNodeConfig.vue
+12
-8
src/components/SimpleProcessDesignerV2/src/nodes-config/UserTaskNodeConfig.vue
+6
-242
src/components/SimpleProcessDesignerV2/src/nodes-config/components/UserTaskListener.vue
+261
-0
src/components/bpmnProcessDesigner/package/designer/plugins/descriptor/flowableDescriptor.json
+14
-0
src/components/bpmnProcessDesigner/package/penal/PropertiesPanel.vue
+1
-1
src/components/bpmnProcessDesigner/package/penal/custom-config/components/UserTaskCustomConfig.vue
+16
-1
src/utils/download.ts
+27
-0
src/views/bpm/processInstance/detail/ProcessInstanceOperationButton.vue
+8
-8
src/views/bpm/processInstance/detail/ProcessInstanceTimeline.vue
+3
-3
src/views/bpm/processInstance/detail/SignDialog.vue
+3
-32
No files found.
src/api/bpm/processInstance/index.ts
View file @
e8e357b8
...
@@ -36,7 +36,7 @@ export type ApprovalTaskInfo = {
...
@@ -36,7 +36,7 @@ export type ApprovalTaskInfo = {
assigneeUser
:
User
assigneeUser
:
User
status
:
number
status
:
number
reason
:
string
reason
:
string
sign
:
string
// TODO @lesan:字段改成 signPicUrl 签名照片。只有 sign 感觉是签名文本哈。
sign
PicUrl
:
string
}
}
// 审批节点信息
// 审批节点信息
...
...
src/components/SimpleProcessDesignerV2/src/nodes-config/RouterNodeConfig.vue
View file @
e8e357b8
...
@@ -86,7 +86,7 @@ const currentNode = useWatchNode(props)
...
@@ -86,7 +86,7 @@ const currentNode = useWatchNode(props)
// 节点名称
// 节点名称
const
{
nodeName
,
showInput
,
clickIcon
,
blurEvent
}
=
useNodeName
(
NodeType
.
ROUTER_BRANCH_NODE
)
const
{
nodeName
,
showInput
,
clickIcon
,
blurEvent
}
=
useNodeName
(
NodeType
.
ROUTER_BRANCH_NODE
)
const
routerGroups
=
ref
<
RouterCondition
[]
>
([])
const
routerGroups
=
ref
<
RouterCondition
[]
>
([])
const
nodeOptions
=
ref
(
)
const
nodeOptions
=
ref
<
any
>
([]
)
const
conditionRef
=
ref
([])
const
conditionRef
=
ref
([])
/** 保存配置 */
/** 保存配置 */
...
@@ -94,7 +94,7 @@ const saveConfig = async () => {
...
@@ -94,7 +94,7 @@ const saveConfig = async () => {
// 校验表单
// 校验表单
let
valid
=
true
let
valid
=
true
for
(
const
item
of
conditionRef
.
value
)
{
for
(
const
item
of
conditionRef
.
value
)
{
if
(
!
(
await
item
.
validate
()))
{
if
(
item
&&
!
(
await
item
.
validate
()))
{
valid
=
false
valid
=
false
}
}
}
}
...
@@ -109,7 +109,7 @@ const saveConfig = async () => {
...
@@ -109,7 +109,7 @@ const saveConfig = async () => {
}
}
// 显示路由分支节点配置, 由父组件传过来
// 显示路由分支节点配置, 由父组件传过来
const
showRouteNodeConfig
=
(
node
:
SimpleFlowNode
)
=>
{
const
showRouteNodeConfig
=
(
node
:
SimpleFlowNode
)
=>
{
getRouterNode
()
getRouterNode
(
processNodeTree
?.
value
)
routerGroups
.
value
=
[]
routerGroups
.
value
=
[]
nodeName
.
value
=
node
.
name
nodeName
.
value
=
node
.
name
if
(
node
.
routerGroups
)
{
if
(
node
.
routerGroups
)
{
...
@@ -172,15 +172,14 @@ const deleteRouterGroup = (index: number) => {
...
@@ -172,15 +172,14 @@ const deleteRouterGroup = (index: number) => {
routerGroups
.
value
.
splice
(
index
,
1
)
routerGroups
.
value
.
splice
(
index
,
1
)
}
}
const
getRouterNode
=
()
=>
{
// 递归获取所有节点
// TODO @lesan 还需要满足以下要求
const
getRouterNode
=
(
node
)
=>
{
// TODO 最好还需要满足以下要求
// 并行分支、包容分支内部节点不能跳转到外部节点
// 并行分支、包容分支内部节点不能跳转到外部节点
// 条件分支节点可以向上跳转到外部节点
// 条件分支节点可以向上跳转到外部节点
let
node
=
processNodeTree
?.
value
nodeOptions
.
value
=
[]
while
(
true
)
{
while
(
true
)
{
if
(
!
node
)
break
if
(
!
node
)
break
if
(
node
.
type
!==
NodeType
.
ROUTER_BRANCH_NODE
)
{
if
(
node
.
type
!==
NodeType
.
ROUTER_BRANCH_NODE
&&
node
.
type
!==
NodeType
.
CONDITION_NODE
)
{
nodeOptions
.
value
.
push
({
nodeOptions
.
value
.
push
({
label
:
node
.
name
,
label
:
node
.
name
,
value
:
node
.
id
value
:
node
.
id
...
@@ -189,6 +188,11 @@ const getRouterNode = () => {
...
@@ -189,6 +188,11 @@ const getRouterNode = () => {
if
(
!
node
.
childNode
||
node
.
type
===
NodeType
.
END_EVENT_NODE
)
{
if
(
!
node
.
childNode
||
node
.
type
===
NodeType
.
END_EVENT_NODE
)
{
break
break
}
}
if
(
node
.
conditionNodes
&&
node
.
conditionNodes
.
length
)
{
node
.
conditionNodes
.
forEach
((
item
)
=>
{
getRouterNode
(
item
)
})
}
node
=
node
.
childNode
node
=
node
.
childNode
}
}
}
}
...
...
src/components/SimpleProcessDesignerV2/src/nodes-config/UserTaskNodeConfig.vue
View file @
e8e357b8
...
@@ -440,217 +440,8 @@
...
@@ -440,217 +440,8 @@
</div>
</div>
</div>
</div>
</el-tab-pane>
</el-tab-pane>
<!-- TODO @lesan:要不抽成 Listener 小组件?类似 Condition.vue -->
<el-tab-pane
label=
"监听器"
name=
"listener"
>
<el-tab-pane
label=
"监听器"
name=
"listener"
>
<el-form
ref=
"listenerFormRef"
:model=
"configForm"
label-position=
"top"
>
<UserTaskListener
ref=
"userTaskListenerRef"
v-model=
"configForm"
:form-field-options=
"formFieldOptions"
/>
<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>
<el-form-item
label=
"请求头"
>
<div
class=
"flex pt-2"
v-for=
"(item, index) in configForm[`task${listener.type}ListenerHeader`]"
:key=
"index"
>
<div
class=
"mr-2"
>
<el-form-item
:prop=
"`task${listener.type}ListenerHeader.${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 LISTENER_MAP_TYPES"
:key=
"types.value"
:label=
"types.label"
:value=
"types.value"
/>
</el-select>
</div>
<div
class=
"mr-2"
>
<el-form-item
:prop=
"`task${listener.type}ListenerHeader.${index}.value`"
:rules=
"{
required: true,
message: '参数值不能为空',
trigger: 'blur'
}"
>
<el-input
v-if=
"item.type === ListenerParamTypeEnum.FIXED_VALUE"
class=
"w-160px"
v-model=
"item.value"
/>
</el-form-item>
<el-form-item
:prop=
"`task${listener.type}ListenerHeader.${index}.value`"
:rules=
"{
required: true,
message: '参数值不能为空',
trigger: 'change'
}"
>
<el-select
v-if=
"item.type === ListenerParamTypeEnum.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=
"
deleteTaskListenerParam(
configForm[`task${listener.type}ListenerHeader`],
index
)
"
/>
</div>
</div>
<el-button
type=
"primary"
text
@
click=
"addTaskListenerParam(configForm[`task${listener.type}ListenerHeader`])"
>
<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 configForm[`task${listener.type}ListenerBody`]"
:key=
"index"
>
<div
class=
"mr-2"
>
<el-form-item
:prop=
"`task${listener.type}ListenerBody.${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 LISTENER_MAP_TYPES"
:key=
"types.value"
:label=
"types.label"
:value=
"types.value"
/>
</el-select>
</div>
<div
class=
"mr-2"
>
<el-form-item
:prop=
"`task${listener.type}ListenerBody.${index}.value`"
:rules=
"{
required: true,
message: '参数值不能为空',
trigger: 'blur'
}"
>
<el-input
v-if=
"item.type === ListenerParamTypeEnum.FIXED_VALUE"
class=
"w-160px"
v-model=
"item.value"
/>
</el-form-item>
<el-form-item
:prop=
"`task${listener.type}ListenerBody.${index}.value`"
:rules=
"{
required: true,
message: '参数值不能为空',
trigger: 'change'
}"
>
<el-select
v-if=
"item.type === ListenerParamTypeEnum.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=
"
deleteTaskListenerParam(
configForm[`task${listener.type}ListenerBody`],
index
)
"
/>
</div>
</div>
<el-button
type=
"primary"
text
@
click=
"addTaskListenerParam(configForm[`task${listener.type}ListenerBody`])"
>
<Icon
icon=
"ep:plus"
class=
"mr-5px"
/>
添加一行
</el-button>
</el-form-item>
</div>
</div>
</el-form>
</el-tab-pane>
</el-tab-pane>
</el-tabs>
</el-tabs>
<
template
#
footer
>
<
template
#
footer
>
...
@@ -687,9 +478,7 @@ import {
...
@@ -687,9 +478,7 @@ import {
ASSIGN_EMPTY_HANDLER_TYPES
,
ASSIGN_EMPTY_HANDLER_TYPES
,
AssignEmptyHandlerType
,
AssignEmptyHandlerType
,
FieldPermissionType
,
FieldPermissionType
,
ProcessVariableEnum
,
ProcessVariableEnum
LISTENER_MAP_TYPES
,
ListenerParamTypeEnum
}
from
'../consts'
}
from
'../consts'
import
{
import
{
...
@@ -703,6 +492,7 @@ import {
...
@@ -703,6 +492,7 @@ import {
import
{
defaultProps
}
from
'@/utils/tree'
import
{
defaultProps
}
from
'@/utils/tree'
import
{
cloneDeep
}
from
'lodash-es'
import
{
cloneDeep
}
from
'lodash-es'
import
{
convertTimeUnit
,
getApproveTypeText
}
from
'../utils'
import
{
convertTimeUnit
,
getApproveTypeText
}
from
'../utils'
import
UserTaskListener
from
'./components/UserTaskListener.vue'
defineOptions
({
defineOptions
({
name
:
'UserTaskNodeConfig'
name
:
'UserTaskNodeConfig'
})
})
...
@@ -780,21 +570,6 @@ const formRules = reactive({
...
@@ -780,21 +570,6 @@ const formRules = reactive({
assignEmptyHandlerUserIds
:
[{
required
:
true
,
message
:
'用户不能为空'
,
trigger
:
'change'
}],
assignEmptyHandlerUserIds
:
[{
required
:
true
,
message
:
'用户不能为空'
,
trigger
:
'change'
}],
assignStartUserHandlerType
:
[{
required
:
true
}]
assignStartUserHandlerType
:
[{
required
:
true
}]
})
})
// 监听器数组
const
taskListener
=
ref
([
{
name
:
'创建任务'
,
type
:
'Create'
},
{
name
:
'指派任务执行人员'
,
type
:
'Assign'
},
{
name
:
'完成任务'
,
type
:
'Complete'
}
])
const
{
const
{
configForm
:
tempConfigForm
,
configForm
:
tempConfigForm
,
...
@@ -843,7 +618,7 @@ const {
...
@@ -843,7 +618,7 @@ const {
cTimeoutMaxRemindCount
cTimeoutMaxRemindCount
}
=
useTimeoutHandler
()
}
=
useTimeoutHandler
()
const
listenerForm
Ref
=
ref
()
const
userTaskListener
Ref
=
ref
()
// 保存配置
// 保存配置
const
saveConfig
=
async
()
=>
{
const
saveConfig
=
async
()
=>
{
...
@@ -860,8 +635,8 @@ const saveConfig = async () => {
...
@@ -860,8 +635,8 @@ const saveConfig = async () => {
}
}
if
(
!
formRef
)
return
false
if
(
!
formRef
)
return
false
if
(
!
listenerForm
Ref
)
return
false
if
(
!
userTaskListener
Ref
)
return
false
const
valid
=
(
await
formRef
.
value
.
validate
())
&&
(
await
listenerForm
Ref
.
value
.
validate
())
const
valid
=
(
await
formRef
.
value
.
validate
())
&&
(
await
userTaskListener
Ref
.
value
.
validate
())
if
(
!
valid
)
return
false
if
(
!
valid
)
return
false
const
showText
=
getShowText
()
const
showText
=
getShowText
()
if
(
!
showText
)
return
false
if
(
!
showText
)
return
false
...
@@ -1104,17 +879,6 @@ function useTimeoutHandler() {
...
@@ -1104,17 +879,6 @@ function useTimeoutHandler() {
cTimeoutMaxRemindCount
cTimeoutMaxRemindCount
}
}
}
}
const
addTaskListenerParam
=
(
arr
)
=>
{
arr
.
push
({
key
:
''
,
type
:
1
,
value
:
''
})
}
const
deleteTaskListenerParam
=
(
arr
,
index
)
=>
{
arr
.
splice
(
index
,
1
)
}
</
script
>
</
script
>
<
style
lang=
"scss"
scoped
>
<
style
lang=
"scss"
scoped
>
...
...
src/components/SimpleProcessDesignerV2/src/nodes-config/components/UserTaskListener.vue
0 → 100644
View file @
e8e357b8
<
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>
<el-form-item
label=
"请求头"
>
<div
class=
"flex pt-2"
v-for=
"(item, index) in configForm[`task$
{listener.type}ListenerHeader`]"
:key="index"
>
<div
class=
"mr-2"
>
<el-form-item
:prop=
"`task$
{listener.type}ListenerHeader.${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 LISTENER_MAP_TYPES"
:key=
"types.value"
:label=
"types.label"
:value=
"types.value"
/>
</el-select>
</div>
<div
class=
"mr-2"
>
<el-form-item
:prop=
"`task$
{listener.type}ListenerHeader.${index}.value`"
:rules="{
required: true,
message: '参数值不能为空',
trigger: 'blur'
}"
>
<el-input
v-if=
"item.type === ListenerParamTypeEnum.FIXED_VALUE"
class=
"w-160px"
v-model=
"item.value"
/>
</el-form-item>
<el-form-item
:prop=
"`task$
{listener.type}ListenerHeader.${index}.value`"
:rules="{
required: true,
message: '参数值不能为空',
trigger: 'change'
}"
>
<el-select
v-if=
"item.type === ListenerParamTypeEnum.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=
"
deleteTaskListenerParam(configForm[`task$
{listener.type}ListenerHeader`], index)
"
/>
</div>
</div>
<el-button
type=
"primary"
text
@
click=
"addTaskListenerParam(configForm[`task$
{listener.type}ListenerHeader`])"
>
<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 configForm[`task$
{listener.type}ListenerBody`]"
:key="index"
>
<div
class=
"mr-2"
>
<el-form-item
:prop=
"`task$
{listener.type}ListenerBody.${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 LISTENER_MAP_TYPES"
:key=
"types.value"
:label=
"types.label"
:value=
"types.value"
/>
</el-select>
</div>
<div
class=
"mr-2"
>
<el-form-item
:prop=
"`task$
{listener.type}ListenerBody.${index}.value`"
:rules="{
required: true,
message: '参数值不能为空',
trigger: 'blur'
}"
>
<el-input
v-if=
"item.type === ListenerParamTypeEnum.FIXED_VALUE"
class=
"w-160px"
v-model=
"item.value"
/>
</el-form-item>
<el-form-item
:prop=
"`task$
{listener.type}ListenerBody.${index}.value`"
:rules="{
required: true,
message: '参数值不能为空',
trigger: 'change'
}"
>
<el-select
v-if=
"item.type === ListenerParamTypeEnum.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=
"
deleteTaskListenerParam(configForm[`task$
{listener.type}ListenerBody`], index)
"
/>
</div>
</div>
<el-button
type=
"primary"
text
@
click=
"addTaskListenerParam(configForm[`task$
{listener.type}ListenerBody`])"
>
<Icon
icon=
"ep:plus"
class=
"mr-5px"
/>
添加一行
</el-button>
</el-form-item>
</div>
</div>
</el-form>
</
template
>
<
script
setup
lang=
"ts"
>
import
{
LISTENER_MAP_TYPES
,
ListenerParamTypeEnum
}
from
'../../consts'
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
addTaskListenerParam
=
(
arr
)
=>
{
arr
.
push
({
key
:
''
,
type
:
1
,
value
:
''
})
}
const
deleteTaskListenerParam
=
(
arr
,
index
)
=>
{
arr
.
splice
(
index
,
1
)
}
const
validate
=
async
()
=>
{
if
(
!
listenerFormRef
)
return
false
return
await
listenerFormRef
.
value
.
validate
()
}
defineExpose
({
validate
})
</
script
>
src/components/bpmnProcessDesigner/package/designer/plugins/descriptor/flowableDescriptor.json
View file @
e8e357b8
...
@@ -1438,6 +1438,20 @@
...
@@ -1438,6 +1438,20 @@
"isBody"
:
true
"isBody"
:
true
}
}
]
]
},
{
"name"
:
"SignEnable"
,
"superClass"
:
[
"Element"
],
"meta"
:
{
"allowedIn"
:
[
"bpmn:UserTask"
]
},
"properties"
:
[
{
"name"
:
"value"
,
"type"
:
"Boolean"
,
"isBody"
:
true
}
]
}
}
],
],
"emumerations"
:
[]
"emumerations"
:
[]
...
...
src/components/bpmnProcessDesigner/package/penal/PropertiesPanel.vue
View file @
e8e357b8
<
template
>
<
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
v-model=
"activeTab"
v-if=
"isReady"
>
<el-collapse-item
name=
"base"
>
<el-collapse-item
name=
"base"
>
<!-- class="panel-tab__title" -->
<!-- class="panel-tab__title" -->
...
...
src/components/bpmnProcessDesigner/package/penal/custom-config/components/UserTaskCustomConfig.vue
View file @
e8e357b8
...
@@ -5,6 +5,7 @@
...
@@ -5,6 +5,7 @@
4. 操作按钮
4. 操作按钮
5. 字段权限
5. 字段权限
6. 审批类型
6. 审批类型
7. 是否需要签名
-->
-->
<
template
>
<
template
>
<div>
<div>
...
@@ -161,6 +162,11 @@
...
@@ -161,6 +162,11 @@
</el-radio-group>
</el-radio-group>
</div>
</div>
</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>
</div>
</div>
</
template
>
</
template
>
...
@@ -218,6 +224,9 @@ const { formType, fieldsPermissionConfig, getNodeConfigFormFields } = useFormFie
...
@@ -218,6 +224,9 @@ const { formType, fieldsPermissionConfig, getNodeConfigFormFields } = useFormFie
// 审批类型
// 审批类型
const
approveType
=
ref
({
value
:
ApproveType
.
USER
})
const
approveType
=
ref
({
value
:
ApproveType
.
USER
})
// 是否需要签名
const
signEnable
=
ref
({
value
:
false
})
const
elExtensionElements
=
ref
()
const
elExtensionElements
=
ref
()
const
otherExtensions
=
ref
()
const
otherExtensions
=
ref
()
const
bpmnElement
=
ref
()
const
bpmnElement
=
ref
()
...
@@ -325,6 +334,11 @@ const resetCustomConfigList = () => {
...
@@ -325,6 +334,11 @@ const resetCustomConfigList = () => {
ex
.
$type
!==
`
${
prefix
}
:ApproveType`
ex
.
$type
!==
`
${
prefix
}
:ApproveType`
)
??
[]
)
??
[]
// 是否需要签名
signEnable
.
value
=
elExtensionElements
.
value
.
values
?.
filter
((
ex
)
=>
ex
.
$type
===
`
${
prefix
}
:SignEnable`
)?.[
0
]
||
bpmnInstances
().
moddle
.
create
(
`
${
prefix
}
:SignEnable`
,
{
value
:
false
})
// 更新元素扩展属性,避免后续报错
// 更新元素扩展属性,避免后续报错
updateElementExtensions
()
updateElementExtensions
()
}
}
...
@@ -373,7 +387,8 @@ const updateElementExtensions = () => {
...
@@ -373,7 +387,8 @@ const updateElementExtensions = () => {
assignEmptyUserIdsEl
.
value
,
assignEmptyUserIdsEl
.
value
,
approveType
.
value
,
approveType
.
value
,
...
buttonsSettingEl
.
value
,
...
buttonsSettingEl
.
value
,
...
fieldsPermissionEl
.
value
...
fieldsPermissionEl
.
value
,
signEnable
.
value
]
]
})
})
bpmnInstances
().
modeling
.
updateProperties
(
toRaw
(
bpmnElement
.
value
),
{
bpmnInstances
().
modeling
.
updateProperties
(
toRaw
(
bpmnElement
.
value
),
{
...
...
src/utils/download.ts
View file @
e8e357b8
...
@@ -65,6 +65,33 @@ const download = {
...
@@ -65,6 +65,33 @@ const download = {
a
.
download
=
'image.png'
a
.
download
=
'image.png'
a
.
click
()
a
.
click
()
}
}
},
base64ToFile
:
(
base64
,
fileName
)
=>
{
// 将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文件对象
// new File(bits, name, options)
const
file
=
new
File
([
u8arr
],
`
${
fileName
}
.
${
suffix
}
`
,
{
type
:
type
})
// 将File文件对象返回给方法的调用者
return
file
}
}
}
}
...
...
src/views/bpm/processInstance/detail/ProcessInstanceOperationButton.vue
View file @
e8e357b8
...
@@ -47,15 +47,15 @@
...
@@ -47,15 +47,15 @@
<el-form-item
<el-form-item
v-if=
"runningTask.signEnable"
v-if=
"runningTask.signEnable"
label=
"签名"
label=
"签名"
prop=
"sign"
prop=
"sign
PicUrl
"
ref=
"approveSignFormRef"
ref=
"approveSignFormRef"
>
>
<el-button
@
click=
"signRef.open()"
>
点击签名
</el-button>
<el-button
@
click=
"signRef.open()"
>
点击签名
</el-button>
<el-image
<el-image
class=
"w-90px h-40px ml-5px"
class=
"w-90px h-40px ml-5px"
v-if=
"approveReasonForm.sign"
v-if=
"approveReasonForm.sign
PicUrl
"
:src=
"approveReasonForm.sign"
:src=
"approveReasonForm.sign
PicUrl
"
:preview-src-list=
"[approveReasonForm.sign]"
:preview-src-list=
"[approveReasonForm.sign
PicUrl
]"
/>
/>
</el-form-item>
</el-form-item>
<el-form-item>
<el-form-item>
...
@@ -553,11 +553,11 @@ const signRef = ref()
...
@@ -553,11 +553,11 @@ const signRef = ref()
const
approveSignFormRef
=
ref
()
const
approveSignFormRef
=
ref
()
const
approveReasonForm
=
reactive
({
const
approveReasonForm
=
reactive
({
reason
:
''
,
reason
:
''
,
sign
:
''
sign
PicUrl
:
''
})
})
const
approveReasonRule
=
reactive
<
FormRules
<
typeof
approveReasonForm
>>
({
const
approveReasonRule
=
reactive
<
FormRules
<
typeof
approveReasonForm
>>
({
reason
:
[{
required
:
true
,
message
:
'审批意见不能为空'
,
trigger
:
'blur'
}],
reason
:
[{
required
:
true
,
message
:
'审批意见不能为空'
,
trigger
:
'blur'
}],
sign
:
[{
required
:
true
,
message
:
'签名不能为空'
,
trigger
:
'change'
}]
sign
PicUrl
:
[{
required
:
true
,
message
:
'签名不能为空'
,
trigger
:
'change'
}]
})
})
// 拒绝表单
// 拒绝表单
const
rejectFormRef
=
ref
<
FormInstance
>
()
const
rejectFormRef
=
ref
<
FormInstance
>
()
...
@@ -705,7 +705,7 @@ const handleAudit = async (pass: boolean, formRef: FormInstance | undefined) =>
...
@@ -705,7 +705,7 @@ const handleAudit = async (pass: boolean, formRef: FormInstance | undefined) =>
}
}
// 签名
// 签名
if
(
runningTask
.
value
.
signEnable
)
{
if
(
runningTask
.
value
.
signEnable
)
{
data
.
sign
=
approveReasonForm
.
sign
data
.
sign
PicUrl
=
approveReasonForm
.
signPicUrl
}
}
// 多表单处理,并且有额外的 approveForm 表单,需要校验 + 拼接到 data 表单里提交
// 多表单处理,并且有额外的 approveForm 表单,需要校验 + 拼接到 data 表单里提交
// TODO 芋艿 任务有多表单这里要如何处理,会和可编辑的字段冲突
// TODO 芋艿 任务有多表单这里要如何处理,会和可编辑的字段冲突
...
@@ -1002,7 +1002,7 @@ const getUpdatedProcessInstanceVariables = () => {
...
@@ -1002,7 +1002,7 @@ const getUpdatedProcessInstanceVariables = () => {
/** 处理签名完成 */
/** 处理签名完成 */
const
handleSignFinish
=
(
url
:
string
)
=>
{
const
handleSignFinish
=
(
url
:
string
)
=>
{
approveReasonForm
.
sign
=
url
approveReasonForm
.
sign
PicUrl
=
url
approveSignFormRef
.
value
.
validate
(
'change'
)
approveSignFormRef
.
value
.
validate
(
'change'
)
}
}
...
...
src/views/bpm/processInstance/detail/ProcessInstanceTimeline.vue
View file @
e8e357b8
...
@@ -124,14 +124,14 @@
...
@@ -124,14 +124,14 @@
审批意见:{{ task.reason }}
审批意见:{{ task.reason }}
</div>
</div>
<div
<div
v-if=
"task.sign && activity.nodeType === NodeType.USER_TASK_NODE"
v-if=
"task.sign
PicUrl
&& activity.nodeType === NodeType.USER_TASK_NODE"
class=
"text-#a5a5a5 text-13px mt-1 w-full bg-#f8f8fa p2 rounded-md"
class=
"text-#a5a5a5 text-13px mt-1 w-full bg-#f8f8fa p2 rounded-md"
>
>
签名:
签名:
<el-image
<el-image
class=
"w-90px h-40px ml-5px"
class=
"w-90px h-40px ml-5px"
:src=
"task.sign"
:src=
"task.sign
PicUrl
"
:preview-src-list=
"[task.sign]"
:preview-src-list=
"[task.sign
PicUrl
]"
/>
/>
</div>
</div>
</teleport>
</teleport>
...
...
src/views/bpm/processInstance/detail/SignDialog.vue
View file @
e8e357b8
...
@@ -2,9 +2,8 @@
...
@@ -2,9 +2,8 @@
<el-dialog
v-model=
"signDialogVisible"
title=
"签名"
width=
"935"
>
<el-dialog
v-model=
"signDialogVisible"
title=
"签名"
width=
"935"
>
<div
class=
"position-relative"
>
<div
class=
"position-relative"
>
<Vue3Signature
class=
"b b-solid b-gray"
ref=
"signature"
w=
"900px"
h=
"400px"
/>
<Vue3Signature
class=
"b b-solid b-gray"
ref=
"signature"
w=
"900px"
h=
"400px"
/>
<!-- @lesan:建议改成 unocss 哈 -->
<el-button
<el-button
style=
"position: absolute; bottom: 20px; right:
10px"
class=
"pos-absolute bottom-20px right-
10px"
type=
"primary"
type=
"primary"
text
text
size=
"small"
size=
"small"
...
@@ -26,6 +25,7 @@
...
@@ -26,6 +25,7 @@
<
script
setup
lang=
"ts"
>
<
script
setup
lang=
"ts"
>
import
Vue3Signature
from
'vue3-signature'
import
Vue3Signature
from
'vue3-signature'
import
*
as
FileApi
from
'@/api/infra/file'
import
*
as
FileApi
from
'@/api/infra/file'
import
download
from
'@/utils/download'
const
message
=
useMessage
()
// 消息弹窗
const
message
=
useMessage
()
// 消息弹窗
const
signDialogVisible
=
ref
(
false
)
const
signDialogVisible
=
ref
(
false
)
...
@@ -40,40 +40,11 @@ const emits = defineEmits(['success'])
...
@@ -40,40 +40,11 @@ const emits = defineEmits(['success'])
const
submit
=
async
()
=>
{
const
submit
=
async
()
=>
{
message
.
success
(
'签名上传中请稍等。。。'
)
message
.
success
(
'签名上传中请稍等。。。'
)
const
res
=
await
FileApi
.
updateFile
({
const
res
=
await
FileApi
.
updateFile
({
file
:
base64ToFile
(
signature
.
value
.
save
(
'image/png'
),
'签名'
)
file
:
download
.
base64ToFile
(
signature
.
value
.
save
(
'image/png'
),
'签名'
)
})
})
emits
(
'success'
,
res
.
data
)
emits
(
'success'
,
res
.
data
)
signDialogVisible
.
value
=
false
signDialogVisible
.
value
=
false
}
}
// TODO @lesan:这个要不抽到 download.js 里,让这个组件更简洁干净?
const
base64ToFile
=
(
base64
,
fileName
)
=>
{
// 将base64按照 , 进行分割 将前缀 与后续内容分隔开
let
data
=
base64
.
split
(
','
)
// 利用正则表达式 从前缀中获取图片的类型信息(image/png、image/jpeg、image/webp等)
let
type
=
data
[
0
].
match
(
/:
(
.*
?)
;/
)[
1
]
// 从图片的类型信息中 获取具体的文件格式后缀(png、jpeg、webp)
let
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文件对象
// new File(bits, name, options)
const
file
=
new
File
([
u8arr
],
`
${
fileName
}
.
${
suffix
}
`
,
{
type
:
type
})
// 将File文件对象返回给方法的调用者
return
file
}
</
script
>
</
script
>
<
style
scoped
></
style
>
<
style
scoped
></
style
>
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment