feat(projects): 新增我的待办功能,新增审批,驳回组件

This commit is contained in:
AN
2025-07-14 22:29:59 +08:00
parent 523aca6b75
commit 130ee1dcec
8 changed files with 389 additions and 39 deletions

View File

@ -192,6 +192,10 @@ function closeModal() {
}
function handleConfirm() {
if (checkedRowKeys.value.length === 0) {
window.$message?.error('请选择用户');
return;
}
// 获取选中行对应的用户对象(从所有页面数据中筛选)
const selectedUsers = allPagesData.value.filter(item => checkedRowKeys.value.includes(item.userId.toString()));
emit('confirm', checkedRowKeys.value, selectedUsers);

View File

@ -0,0 +1,142 @@
<script lang="ts" setup>
import { reactive, ref, watch } from 'vue';
import type { UploadFileInfo } from 'naive-ui';
import { useLoading } from '@sa/hooks';
import { messageTypeOptions } from '@/constants/workflow';
import { fetchBackTask, fetchGetBackNode } from '@/service/api/workflow/task';
defineOptions({
name: 'BackTaskModal'
});
interface Props {
task: Api.Workflow.Task;
}
const props = defineProps<Props>();
interface Emits {
(e: 'submit'): void;
}
const emit = defineEmits<Emits>();
const visible = defineModel<boolean>('visible', {
default: false
});
const title = defineModel<string>('title', {
default: '驳回'
});
const { loading: backFormLoading, startLoading: startBackFormLoading, endLoading: endBackFormLoading } = useLoading();
const { loading: backBtnLoading, startLoading: startBackBtnLoading, endLoading: endBackBtnLoading } = useLoading();
const accept = ref<string>('.doc,.docx,.xls,.xlsx,.ppt,.pptx,.txt,.pdf,.jpg,.jpeg,.png,.gif,.bmp,.webp');
type Model = Api.Workflow.BackOperateParams;
const backModel = reactive(createBackModel());
function createBackModel(): Model {
return {
taskId: null,
fileId: null,
messageType: ['1'],
nodeCode: null,
message: null,
notice: null,
variables: null
};
}
const fileList = ref<UploadFileInfo[]>([]);
const backTaskNodeOptions = ref<CommonType.Option<string, string>[]>([]);
async function initDefault() {
startBackFormLoading();
startBackBtnLoading();
Object.assign(backModel, createBackModel());
const { error, data } = await fetchGetBackNode(props.task.definitionId, props.task.nodeCode);
endBackFormLoading();
endBackBtnLoading();
if (error) return;
backTaskNodeOptions.value = data.map(item => ({
label: item.nodeName,
value: item.nodeCode
}));
backModel.nodeCode = data[0].nodeCode;
}
async function handleSubmit() {
backModel.taskId = props.task.id;
if (fileList.value?.length) {
const fileIds = fileList.value.map(item => item.id);
backModel.fileId = fileIds.join(',');
}
window.$dialog?.warning({
title: '提示',
content: `是否确认驳回?`,
positiveText: `确认驳回`,
positiveButtonProps: {
type: 'primary'
},
negativeText: '取消',
onPositiveClick: async () => {
startBackBtnLoading();
startBackFormLoading();
const { error } = await fetchBackTask(backModel);
endBackBtnLoading();
endBackFormLoading();
if (error) return;
window.$message?.success('驳回成功');
closeDrawer();
emit('submit');
}
});
}
function closeDrawer() {
visible.value = false;
}
watch(visible, () => {
if (visible.value) {
initDefault();
}
});
</script>
<template>
<NModal v-model:show="visible" preset="card" class="w-800px" :title="title" :native-scrollbar="false" closable>
<NSpin :show="backFormLoading">
<NForm v-if="task.flowStatus === 'waiting'" :model="backModel">
<NFormItem label="驳回节点" path="nodeCode">
<NSelect v-model:value="backModel.nodeCode" :options="backTaskNodeOptions" />
</NFormItem>
<NFormItem label="通知方式" path="messageType">
<NCheckboxGroup v-model:value="backModel.messageType">
<NSpace item-style="display: flex;">
<NCheckbox
v-for="item in messageTypeOptions"
:key="item.value"
:disabled="item.value === '1'"
:value="item.value"
:label="item.label"
/>
</NSpace>
</NCheckboxGroup>
</NFormItem>
<NFormItem label="附件" path="fileId">
<FileUpload v-model:file-list="fileList" :file-size="20" :max="20" upload-type="file" :accept="accept" />
</NFormItem>
<NFormItem label="审批意见" path="message">
<NInput v-model:value="backModel.message" type="textarea" />
</NFormItem>
</NForm>
</NSpin>
<template #footer>
<NSpace justify="end" :size="16">
<NButton @click="closeDrawer">{{ $t('common.cancel') }}</NButton>
<NButton :loading="backBtnLoading" type="primary" @click="handleSubmit">{{ $t('common.confirm') }}</NButton>
</NSpace>
</template>
</NModal>
</template>

View File

@ -2,6 +2,7 @@
import { computed, reactive, ref, watch } from 'vue';
import { useBoolean, useLoading } from '@sa/hooks';
import { fetchGetTask, fetchTaskOperate, fetchTerminateTask } from '@/service/api/workflow/task';
import ReduceSignatureModal from './reduce-signature-modal.vue';
defineOptions({
name: 'FlowInterveneModal'
@ -59,10 +60,6 @@ function createDefaultTerminateModel(): TerminateModel {
}
function handleTransferConfirm(ids: CommonType.IdType[]) {
if (ids.length === 0) {
window.$message?.error('请选择转办用户');
return;
}
model.userId = ids[0];
model.taskId = props.taskId;
window.$dialog?.warning({
@ -84,10 +81,6 @@ function handleTransferConfirm(ids: CommonType.IdType[]) {
}
function handleAddSignatureConfirm(ids: CommonType.IdType[]) {
if (ids.length === 0) {
window.$message?.error('请选择加签用户');
return;
}
model.userIds = ids;
window.$dialog?.warning({
title: '提示',
@ -111,7 +104,7 @@ function handleTerminate() {
terminateModel.taskId = props.taskId;
window.$dialog?.warning({
title: '提示',
content: '是否确认止?',
content: '是否确认止?',
positiveText: '确认',
positiveButtonProps: {
type: 'primary'
@ -120,7 +113,7 @@ function handleTerminate() {
onPositiveClick: async () => {
const { error } = await fetchTerminateTask(terminateModel);
if (error) return;
window.$message?.success('止成功');
window.$message?.success('止成功');
visible.value = false;
emit('refresh');
}
@ -189,7 +182,7 @@ watch(visible, () => {
<NButton v-if="isWaiting && isTicketOrSignInstance" type="primary" @click="openReduceSignatureModal">
减签
</NButton>
<NButton v-if="isWaiting" type="error" @click="handleTerminate"></NButton>
<NButton v-if="isWaiting" type="error" @click="handleTerminate"></NButton>
</NSpace>
</template>
<!-- 转办用户选择器 -->
@ -202,7 +195,7 @@ watch(visible, () => {
@confirm="handleAddSignatureConfirm"
/>
<!-- 减签用户 -->
<ReduceSignatureDrawer
<ReduceSignatureModal
v-model:visible="reduceSignatureVisible"
:task="taskInfo!"
@reduce-submit="handleReduceSubmit"

View File

@ -1,19 +1,27 @@
<script setup lang="ts">
import { reactive, ref, watch } from 'vue';
import { computed, reactive, ref, watch } from 'vue';
import type { UploadFileInfo } from 'naive-ui';
import { useBoolean, useLoading } from '@sa/hooks';
import { messageTypeOptions } from '@/constants/workflow';
import { fetchCompleteTask, fetchGetTask, fetchGetkNextNode } from '@/service/api/workflow';
import {
fetchCompleteTask,
fetchGetTask,
fetchGetkNextNode,
fetchTaskOperate,
fetchTerminateTask
} from '@/service/api/workflow';
import FileUpload from '@/components/custom/file-upload.vue';
import ReduceSignatureModal from './reduce-signature-modal.vue';
import BackTaskModal from './back-task-modal.vue';
defineOptions({
name: 'FlowTaskApprovalModal'
});
interface Props {
/** the task id */
/** 任务id */
taskId: CommonType.IdType;
/** the task variables */
/** 任务变量 */
taskVariables: { [key: string]: any };
}
@ -32,6 +40,11 @@ const { loading: baseFormLoading, startLoading: startBaseFormLoading, endLoading
const { loading: btnLoading, startLoading: startBtnLoading, endLoading: endBtnLoading } = useLoading();
const { bool: copyVisible, setTrue: openCopyModal } = useBoolean();
const { bool: assigneeVisible, setTrue: openAssigneeModal } = useBoolean();
const { bool: delegateVisible, setTrue: openDelegateModal, setFalse: closeDelegateModal } = useBoolean();
const { bool: transferVisible, setTrue: openTransferModal, setFalse: closeTransferModal } = useBoolean();
const { bool: addSignatureVisible, setTrue: openAddSignatureModal, setFalse: closeAddSignatureModal } = useBoolean();
const { bool: reduceSignatureVisible, setTrue: openReduceSignatureModal } = useBoolean();
const { bool: backVisible, setTrue: openBackModal } = useBoolean();
const title = defineModel<string>('title', {
default: '流程发起'
});
@ -42,6 +55,8 @@ type Model = Api.Workflow.CompleteTaskOperateParams;
const task = ref<Api.Workflow.Task>();
const isWaiting = computed(() => task.value?.flowStatus === 'waiting');
const model: Model = reactive(createDefaultModel());
function createDefaultModel(): Model {
@ -119,19 +134,18 @@ async function getTask() {
}
task.value = data;
task.value.buttonList.forEach(item => {
buttonPerm[item.code as keyof ButtonPerm] = !item.show;
buttonPerm[item.code as keyof ButtonPerm] = item.show!;
});
endBtnLoading();
const { error: nextNodeError, data: nextNodeData } = await fetchGetkNextNode({
taskId: props.taskId,
taskVariables: props.taskVariables
});
endBaseFormLoading();
if (nextNodeError) {
endBaseFormLoading();
return;
}
nestNodeList.value = nextNodeData;
endBaseFormLoading();
}
async function handleSubmit() {
@ -231,6 +245,118 @@ function handleAssigneeTagClose(code: string, index?: number) {
}
}
interface TaskOperationOptions {
userIds: CommonType.IdType[];
operation: 'delegateTask' | 'transferTask' | 'addSignature';
confirmText: string;
successMessage: string;
closeModal: () => void;
}
function handleTaskOperationConfirm(options: TaskOperationOptions) {
const { userIds, operation, confirmText, successMessage, closeModal } = options;
const taskModel = {
taskId: props.taskId,
userId: userIds[0],
message: model.message
};
window.$dialog?.warning({
title: '提示',
content: `是否确认${confirmText}?`,
positiveText: `确认${confirmText}`,
positiveButtonProps: {
type: 'primary'
},
negativeText: '取消',
onPositiveClick: async () => {
startBtnLoading();
startBaseFormLoading();
const { error } = await fetchTaskOperate(taskModel, operation);
endBtnLoading();
endBaseFormLoading();
if (error) return;
window.$message?.success(successMessage);
closeModal();
visible.value = false;
emit('finished');
}
});
}
// 委托
function handleDelegateConfirm(userIds: CommonType.IdType[]) {
handleTaskOperationConfirm({
userIds,
operation: 'delegateTask',
confirmText: '委托',
successMessage: '委托成功',
closeModal: closeDelegateModal
});
}
// 转办
function handleTransferConfirm(userIds: CommonType.IdType[]) {
handleTaskOperationConfirm({
userIds,
operation: 'transferTask',
confirmText: '转办',
successMessage: '转办成功',
closeModal: closeTransferModal
});
}
// 加签
function handleAddSignatureConfirm(userIds: CommonType.IdType[]) {
handleTaskOperationConfirm({
userIds,
operation: 'addSignature',
confirmText: '加签',
successMessage: '加签成功',
closeModal: closeAddSignatureModal
});
}
// 减签
function handleReduceSubmit() {
visible.value = false;
emit('finished');
}
// 终止
function handleTerminate() {
const terminateModel = {
taskId: props.taskId,
comment: model.message
};
window.$dialog?.warning({
title: '提示',
content: '是否确认终止?',
positiveText: '确认',
positiveButtonProps: {
type: 'primary'
},
negativeText: '取消',
onPositiveClick: async () => {
startBtnLoading();
startBaseFormLoading();
const { error } = await fetchTerminateTask(terminateModel);
endBtnLoading();
endBaseFormLoading();
if (error) return;
window.$message?.success('终止成功');
visible.value = false;
emit('finished');
}
});
}
function handleBackSubmit() {
visible.value = false;
emit('finished');
}
watch(visible, () => {
if (visible.value) {
initDefault();
@ -291,22 +417,80 @@ watch(visible, () => {
</div>
</NSpace>
</NFormItem>
<NFormItem v-if="task?.flowStatus === 'waiting'" label="审批意见" path="message">
<NFormItem v-if="isWaiting" label="审批意见" path="message">
<NInput v-model:value="model.message" type="textarea" />
</NFormItem>
</NForm>
<div class="flex justify-end gap-12px">
<NButton @click="visible = false">取消</NButton>
<NButton :loading="btnLoading" type="primary" @click="handleSubmit">提交</NButton>
</div>
</NSpin>
<template #footer>
<NSpace justify="end" :size="16">
<NButton @click="visible = false">{{ $t('common.cancel') }}</NButton>
<!-- 委托 -->
<NButton v-if="isWaiting && buttonPerm.trust" :loading="btnLoading" type="warning" @click="openDelegateModal">
委托
</NButton>
<!-- 转办 -->
<NButton
v-if="isWaiting && buttonPerm.transfer"
:loading="btnLoading"
type="warning"
@click="openTransferModal"
>
转办
</NButton>
<!-- 加签 -->
<NButton
v-if="isWaiting && buttonPerm.addSign && Number(task?.nodeRatio) > 0"
:loading="btnLoading"
type="warning"
@click="openAddSignatureModal"
>
加签
</NButton>
<!-- 减签 -->
<NButton
v-if="isWaiting && buttonPerm.subSign && Number(task?.nodeRatio) > 0"
:loading="btnLoading"
type="warning"
@click="openReduceSignatureModal"
>
减签
</NButton>
<!-- 终止 -->
<NButton v-if="isWaiting && buttonPerm.termination" :loading="btnLoading" type="error" @click="handleTerminate">
终止
</NButton>
<!-- 驳回 -->
<NButton v-if="isWaiting && buttonPerm.back" :loading="btnLoading" type="error" @click="openBackModal">
驳回
</NButton>
<NButton :loading="btnLoading" type="primary" @click="handleSubmit">提交</NButton>
</NSpace>
</template>
<!-- 抄送人员选择 -->
<UserSelectModal
v-model:visible="copyVisible"
:row-keys="selectCopyUserIds"
multiple
@confirm="handleCopyConfirm"
/>
<!-- 下一步审批人员选择 -->
<UserSelectModal
v-model:visible="assigneeVisible"
:row-keys="selectAssigneeIds"
:search-user-ids="assigneeSearchUserIds"
multiple
@confirm="handleAssigneeConfirm"
/>
<!-- 转办 -->
<UserSelectModal v-model:visible="transferVisible" @confirm="handleTransferConfirm" />
<!-- 委托 -->
<UserSelectModal v-model:visible="delegateVisible" @confirm="handleDelegateConfirm" />
<!-- 加签 -->
<UserSelectModal v-model:visible="addSignatureVisible" @confirm="handleAddSignatureConfirm" />
<!-- 减签 -->
<ReduceSignatureModal v-model:visible="reduceSignatureVisible" :task="task!" @reduce-submit="handleReduceSubmit" />
<!-- 驳回 -->
<BackTaskModal v-model:visible="backVisible" :task="task!" @submit="handleBackSubmit" />
</NModal>
<UserSelectModal v-model:visible="copyVisible" :row-keys="selectCopyUserIds" multiple @confirm="handleCopyConfirm" />
<UserSelectModal
v-model:visible="assigneeVisible"
:row-keys="selectAssigneeIds"
:search-user-ids="assigneeSearchUserIds"
multiple
@confirm="handleAssigneeConfirm"
/>
</template>

View File

@ -72,8 +72,8 @@ const columns = ref<NaiveUI.TableColumn<UserTaskModel>[]>([
text
type="error"
icon="material-symbols:delete-outline"
tooltipContent={$t('common.delete')}
popconfirmContent={$t('common.confirmDelete')}
tooltipContent={'减签'}
popconfirmContent={'是否确认减签?'}
onPositiveClick={() => handleReduceSignature([row.userId])}
/>
);
@ -128,7 +128,7 @@ watch(visible, async () => {
<template #icon>
<icon-material-symbols:delete-outline class="text-icon" />
</template>
删除
批量减签
</NButton>
</NSpace>
<NDataTable

View File

@ -28,7 +28,7 @@ export function fetchGetkNextNode(data: Api.Workflow.TaskNextNodeSearchParams) {
/** 完成任务 */
export function fetchCompleteTask(data: Api.Workflow.CompleteTaskOperateParams) {
return request<Api.Workflow.Task>({
return request<boolean>({
url: '/workflow/task/completeTask',
method: 'post',
data
@ -55,7 +55,7 @@ export function fetchGetAllFinishedTask(data: Api.Workflow.TaskSearchParams) {
/** 任务操作 */
export function fetchTaskOperate(data: Api.Workflow.TaskOperateParams, operateType: Api.Workflow.TaskOperateType) {
return request<Api.Workflow.Task>({
return request<boolean>({
url: `/workflow/task/taskOperation/${operateType}`,
method: 'post',
data
@ -87,3 +87,18 @@ export function fetchGetTaskWaitList(data: Api.Workflow.TaskSearchParams) {
params: data
});
}
/** 获取可驳回节点 */
export function fetchGetBackNode(definitionId: CommonType.IdType, nodeCode: string) {
return request<Api.Workflow.FlowNodeList>({
url: `/workflow/task/getBackTaskNode/${definitionId}/${nodeCode}`,
method: 'get'
});
}
/** 驳回任务 */
export function fetchBackTask(data: Api.Workflow.BackOperateParams) {
return request<boolean>({
url: '/workflow/task/backProcess',
method: 'post',
data
});
}

View File

@ -461,7 +461,7 @@ declare namespace Api {
}>;
/** 工作流节点 */
type FlowNode = Common.CommonRecord<{
type FlowNode = Common.CommonTenantRecord<{
/** 节点ID */
id: CommonType.IdType;
/** 删除标志 */
@ -502,5 +502,16 @@ declare namespace Api {
/** 工作流节点列表 */
type FlowNodeList = FlowNode[];
/** 驳回操作参数 */
type BackOperateParams = CommonType.RecordNullable<{
taskId: CommonType.IdType;
fileId: CommonType.IdType;
messageType: string[];
nodeCode: string;
message: string;
notice: string;
variables: { [key: string]: any };
}>;
}
}

View File

@ -10,6 +10,7 @@ declare module 'vue' {
export interface GlobalComponents {
AppProvider: typeof import('./../components/common/app-provider.vue')['default']
ApprovalInfoPanel: typeof import('./../components/custom/workflow/approval-info-panel.vue')['default']
BackTaskModal: typeof import('./../components/custom/workflow/back-task-modal.vue')['default']
BetterScroll: typeof import('./../components/custom/better-scroll.vue')['default']
BooleanTag: typeof import('./../components/custom/boolean-tag.vue')['default']
ButtonIcon: typeof import('./../components/custom/button-icon.vue')['default']
@ -145,7 +146,7 @@ declare module 'vue' {
OssUpload: typeof import('./../components/custom/oss-upload.vue')['default']
PinToggler: typeof import('./../components/common/pin-toggler.vue')['default']
PostSelect: typeof import('./../components/custom/post-select.vue')['default']
ReduceSignatureDrawer: typeof import('./../components/custom/workflow/reduce-signature-drawer.vue')['default']
ReduceSignatureModal: typeof import('./../components/custom/workflow/reduce-signature-modal.vue')['default']
ReloadButton: typeof import('./../components/common/reload-button.vue')['default']
RoleSelect: typeof import('./../components/custom/role-select.vue')['default']
RouterLink: typeof import('vue-router')['RouterLink']