feat: [bpm] 同意增加选择下一个审批人和签名操作

This commit is contained in:
jason
2026-01-21 15:09:59 +08:00
parent 3b9696ac03
commit 8aa8b04354
7 changed files with 247 additions and 42 deletions

View File

@@ -134,3 +134,8 @@ export function getProcessInstanceManagerPage(params: PageParam) {
export function cancelProcessInstanceByAdmin(id: string, reason: string) {
return http.delete<boolean>('/bpm/process-instance/cancel-by-admin', { id, reason })
}
/** 获取下一个节点审批人 */
export function getNextApproveNodes(params) {
return http.get<ApprovalNodeInfo[]>('/bpm/process-instance/get-next-approval-nodes', params)
}

View File

@@ -31,6 +31,7 @@ export interface Task {
processInstanceId?: string // 流程实例 ID
processInstance: ProcessInstance
reasonRequire?: boolean // 是否填写审批意见
signEnable?: boolean // 是否需要签名
buttonsSetting?: Record<number, OperationButtonSetting> // 按钮设置
children?: Task[] // 由加签生成,包含多层子任务
}
@@ -46,7 +47,12 @@ export function getTaskDonePage(params: PageParam) {
}
/** 审批通过 */
export function approveTask(data: { id: string, reason: string }) {
export function approveTask(data: {
id: string
reason: string
signPicUrl?: string // 签名图片 URL
nextAssignees?: Record<string, number[]> // 下一个节点审批人
}) {
return http.put<boolean>('/bpm/task/approve', data)
}

View File

@@ -61,7 +61,6 @@
v-if="activityNodes.length > 0"
:activity-nodes="activityNodes"
:show-status-icon="false"
:enable-approve-user-select="true"
@select-user-confirm="selectUserConfirm"
/>

View File

@@ -1,6 +1,5 @@
<template>
<view class="yd-page-container">
<!-- TODO @jason还有一些细节在审批通过没搞完1签名2选择审批人3其它等等 -->
<!-- 顶部导航栏 -->
<wd-navbar
:title="isApprove ? '审批同意' : '审批拒绝'"
@@ -12,6 +11,40 @@
<view class="p-24rpx">
<wd-form ref="formRef" :model="formData" :rules="formRules">
<wd-cell-group border>
<!-- 下一个节点的审批人 -->
<view v-if="isApprove && nextAssigneesActivityNode.length > 0" class="p-24rpx">
<view class="mb-16rpx flex items-center">
<text class="mr-8rpx text-[#f56c6c]">*</text>
<text class="text-28rpx text-[#333]">下一个节点的审批人</text>
</view>
<ProcessInstanceTimeline
:activity-nodes="nextAssigneesActivityNode"
:show-status-icon="false"
:enable-approve-user-select="true"
@select-user-confirm="selectNextAssigneesConfirm"
/>
</view>
<!-- 签名 -->
<view v-if="isApprove && taskInfo?.signEnable" class="border-b border-[#eee] p-24rpx">
<view class="mb-16rpx flex items-center">
<text class="mr-8rpx text-[#f56c6c]">*</text>
<text class="text-28rpx text-[#333]">签名</text>
</view>
<view class="flex items-center gap-16rpx">
<wd-button type="primary" size="small" @click="openSignatureModal">
{{ formData.signPicUrl ? '重新签名' : '点击签名' }}
</wd-button>
<image
v-if="formData.signPicUrl"
:src="formData.signPicUrl"
class="h-80rpx w-192rpx"
mode="aspectFit"
@click="previewSignature"
/>
</view>
</view>
<!-- 审批意见 -->
<wd-textarea
v-model="formData.reason"
@@ -39,15 +72,39 @@
</wd-button>
</view>
</view>
<!-- 签名弹窗 -->
<wd-popup v-model="showSignatureModal" position="bottom" custom-style="height: 60vh;">
<view class="h-full flex flex-col">
<view class="flex items-center justify-between border-b border-[#eee] p-24rpx">
<text class="text-32rpx text-[#333] font-bold">手写签名</text>
<wd-icon name="close" size="40rpx" @click="showSignatureModal = false" />
</view>
<view class="flex-1 p-24rpx">
<wd-signature
:height="300"
:export-scale="2"
background-color="#ffffff"
@confirm="handleSignatureConfirm"
@clear="handleSignatureClear"
/>
</view>
</view>
</wd-popup>
</view>
</template>
<script lang="ts" setup>
import type { FormInstance } from 'wot-design-uni/components/wd-form/types'
import type { ApprovalNodeInfo } from '@/api/bpm/processInstance'
import type { Task } from '@/api/bpm/task'
import { computed, onMounted, reactive, ref } from 'vue'
import { useToast } from 'wot-design-uni'
import { getApprovalDetail, getNextApproveNodes } from '@/api/bpm/processInstance'
import { approveTask, rejectTask } from '@/api/bpm/task'
import { navigateBackPlus } from '@/utils'
import ProcessInstanceTimeline from '@/pages-bpm/processInstance/detail/components/time-line.vue'
import { getEnvBaseUrl, navigateBackPlus } from '@/utils'
import { BpmCandidateStrategyEnum } from '@/utils/constants'
const props = defineProps<{
processInstanceId?: string
@@ -61,49 +118,185 @@ definePage({
navigationStyle: 'custom',
},
})
const taskId = computed(() => props.taskId || '')
const processInstanceId = computed(() => props.processInstanceId)
const isPass = computed(() => props.pass !== 'false') // true: 同意, false: 拒绝
const isApprove = computed(() => props.pass !== 'false') // true: 同意, false: 拒绝
const toast = useToast()
const formLoading = ref(false)
const taskInfo = ref<Task | null>(null) // 任务信息
const nextAssigneesActivityNode = ref<ApprovalNodeInfo[]>([]) // 下一个节点审批人列表
const approveUserSelectTasks = ref<ApprovalNodeInfo[]>([]) // 需要选择审批人的节点列表
const approveUserSelectAssignees = ref<Record<string, number[]>>({}) // 审批人选择的审批人数据
// 签名相关
const showSignatureModal = ref(false)
const formData = reactive({
reason: '',
signPicUrl: '', // 签名图片 URL
})
const formRules = {
reason: [
{ required: true, message: '审批意见不能为空' },
],
}
const formRules = computed(() => {
let rules = {}
if (taskInfo.value?.reasonRequire) {
rules = {
reason: [
{ required: true, message: '审批意见不能为空' },
],
}
}
return rules
})
const formRef = ref<FormInstance>()
/** 是否为同意操作 */
const isApprove = computed(() => isPass.value)
/** 返回上一页 */
function handleBack() {
navigateBackPlus(`/pages-bpm/processInstance/detail/index?id=${processInstanceId.value}&taskId=${taskId.value}`)
}
/** 加载任务信息 */
async function loadTaskInfo() {
const data = await getApprovalDetail({
processInstanceId: processInstanceId.value,
taskId: taskId.value,
})
taskInfo.value = data?.todoTask || null
}
/** 加载下一个节点审批人 */
async function loadNextApproveNodes() {
if (!isApprove.value) {
return
}
const params = {
processInstanceId: processInstanceId.value,
taskId: taskId.value,
}
const data = await getNextApproveNodes(params)
if (data && data.length > 0) {
nextAssigneesActivityNode.value = data
// 获取审批人自选的任务
approveUserSelectTasks.value = data.filter(
(node: ApprovalNodeInfo) =>
BpmCandidateStrategyEnum.APPROVE_USER_SELECT === node.candidateStrategy,
) || []
}
}
/** 选择下一个节点审批人确认 */
function selectNextAssigneesConfirm(activityId: string, userList: any[]) {
approveUserSelectAssignees.value[activityId] = userList.map(user => user.id)
}
/** 打开签名弹窗 */
function openSignatureModal() {
showSignatureModal.value = true
}
/** 签名确认 */
async function handleSignatureConfirm(result: { tempFilePath: string, base64: string }) {
toast.loading('上传中...')
try {
// 上传签名图片
const url = await uploadSignatureFile(result.tempFilePath)
formData.signPicUrl = url
showSignatureModal.value = false
toast.success('签名成功')
} catch (err) {
console.error('上传失败:', err)
toast.show('上传失败')
}
}
/** 上传签名文件 */
function uploadSignatureFile(tempFilePath: string): Promise<string> {
return new Promise((resolve, reject) => {
uni.uploadFile({
url: `${getEnvBaseUrl()}/infra/file/upload`,
filePath: tempFilePath,
name: 'file',
success: (uploadFileRes) => {
try {
const data = JSON.parse(uploadFileRes.data)
if (data.code === 0 && data.data) {
resolve(data.data)
} else {
reject(new Error(data.msg || '上传失败'))
}
} catch (err) {
reject(err)
}
},
fail: (err) => {
console.error('上传失败:', err)
reject(err)
},
})
})
}
/** 签名清除 */
function handleSignatureClear() {
formData.signPicUrl = ''
}
/** 预览签名 */
function previewSignature() {
if (formData.signPicUrl) {
uni.previewImage({
urls: [formData.signPicUrl],
current: formData.signPicUrl,
})
}
}
/** 提交审批 */
async function handleSubmit() {
if (formLoading.value) {
return
}
const { valid } = await formRef.value!.validate()
if (!valid) {
return
}
// 验证签名
if (isApprove.value && taskInfo.value?.signEnable && !formData.signPicUrl) {
toast.show('请先进行签名')
return
}
// 验证审批人选择
if (isApprove.value && approveUserSelectTasks.value.length > 0) {
for (const task of approveUserSelectTasks.value) {
if (!approveUserSelectAssignees.value[task.id] || approveUserSelectAssignees.value[task.id].length === 0) {
toast.show(`请选择「${task.name}」的审批人`)
return
}
}
}
formLoading.value = true
try {
const api = isApprove.value ? approveTask : rejectTask
await api({
id: taskId.value as string,
reason: formData.reason,
})
if (isApprove.value) {
// 审批通过
await approveTask({
id: taskId.value as string,
reason: formData.reason,
signPicUrl: formData.signPicUrl || undefined,
nextAssignees: Object.keys(approveUserSelectAssignees.value).length > 0
? approveUserSelectAssignees.value
: undefined,
})
} else {
// 审批拒绝
await rejectTask({
id: taskId.value as string,
reason: formData.reason,
})
}
toast.success('审批成功')
setTimeout(() => {
uni.redirectTo({
@@ -116,10 +309,19 @@ async function handleSubmit() {
}
/** 页面加载时 */
onMounted(() => {
onMounted(async () => {
/** 初始化校验 */
if (!props.taskId || !props.processInstanceId) {
toast.show('参数错误')
return
}
try {
toast.loading('加载中...')
// 加载任务信息和下一个节点审批人
await loadTaskInfo()
await loadNextApproveNodes()
} finally {
toast.close()
}
})
</script>

View File

@@ -89,15 +89,10 @@ const operationIconsMap: Record<number, string> = {
}
const userStore = useUserStore()
// TODO @jason字段注释使用尾注释哈
/** 左侧操作按钮 【最多两个】{转办, 委派, 退回, 加签, 抄送等} */
const leftOperations = ref<LeftOperationType[]>([])
/** 右侧操作按钮【最多两个】{通过,拒绝, 取消} */
const rightOperationTypes = []
const leftOperations = ref<LeftOperationType[]>([]) // 左侧操作按钮 【最多两个】{转办, 委派, 退回, 加签, 抄送等}
const rightOperationTypes = [] // 右侧操作按钮【最多两个】{通过,拒绝, 取消}
const rightOperations = ref<RightOperationType[]>([])
/** 更多操作 */
const moreOperations = ref<MoreOperationType[]>([])
const moreOperations = ref<MoreOperationType[]>([]) // 更多操作
const runningTask = ref<Task>()
const processInstance = ref<ProcessInstance>()
const reasonRequire = ref<boolean>(false)

View File

@@ -37,8 +37,8 @@
<wd-button
type="primary"
block
:loading="submitting"
:disabled="submitting"
:loading="formLoading"
:disabled="formLoading"
@click="handleSubmit"
>
{{ isDelegate ? '委派' : '转办' }}
@@ -75,7 +75,7 @@ const processInstanceId = computed(() => props.processInstanceId)
const operationType = computed(() => props.type || 'transfer') // 默认转办
const isDelegate = computed(() => operationType.value === 'delegate')
const toast = useToast()
const submitting = ref(false)
const formLoading = ref(false)
const formData = reactive({
userId: undefined as number | undefined,
reason: '',
@@ -97,7 +97,7 @@ function handleBack() {
/** 提交操作 */
async function handleSubmit() {
if (submitting.value) {
if (formLoading.value) {
return
}
const { valid } = await formRef.value!.validate()
@@ -105,8 +105,7 @@ async function handleSubmit() {
return
}
// TODO @jasonsubmitting 改成 formLoading 哇?统一代码风格哈;
submitting.value = true
formLoading.value = true
try {
const data = {
id: taskId.value as string,
@@ -130,7 +129,7 @@ async function handleSubmit() {
})
}, 500)
} finally {
submitting.value = false
formLoading.value = false
}
}

View File

@@ -39,8 +39,8 @@
<wd-button
type="primary"
block
:loading="submitting"
:disabled="submitting"
:loading="formLoading"
:disabled="formLoading"
@click="handleSubmit"
>
退回
@@ -73,7 +73,7 @@ definePage({
const taskId = computed(() => props.taskId)
const processInstanceId = computed(() => props.processInstanceId)
const toast = useToast()
const submitting = ref(false)
const formLoading = ref(false)
const activityOptions = ref<any[]>([])
const formData = reactive({
targetActivityId: '',
@@ -102,15 +102,14 @@ async function loadReturnTaskList() {
/** 提交操作 */
async function handleSubmit() {
if (submitting.value) {
if (formLoading.value) {
return
}
const { valid } = await formRef.value!.validate()
if (!valid) {
return
}
// TODO @jasonsubmitting 改成 formLoading 哇?统一代码风格哈;
submitting.value = true
formLoading.value = true
try {
await returnTask({
id: taskId.value as string,
@@ -125,7 +124,7 @@ async function handleSubmit() {
})
}, 500)
} finally {
submitting.value = false
formLoading.value = false
}
}