feat(approval): PC 待办审批页面与改派 (#22)
- 新增 views/approval/todo:ele-pro-table 待办列表(单号/单据类型/标题/发起人/
报送部门/状态/提交时间/当前节点,状态与关键词筛选)
- 详情抽屉:只读字段、附件、处理记录时间线、当前节点;通过/驳回/存档/归档/
改派动作按 permissions 与 v-permission 显隐,驳回意见必填,改派需选人并填原因
- 新增 api/approval/{model,instance,task},统一走 THINK_API_URL
This commit is contained in:
@@ -0,0 +1,37 @@
|
||||
import request from '@/utils/request';
|
||||
import type { ApiResult, PageResult } from '@/api';
|
||||
import { THINK_API_URL } from '@/config/setting';
|
||||
import type {
|
||||
ApprovalInstance,
|
||||
ApprovalItem,
|
||||
ApprovalItemParam
|
||||
} from '../model';
|
||||
|
||||
/**
|
||||
* 我的待办分页
|
||||
*/
|
||||
export async function pageApprovalTodo(params: ApprovalItemParam) {
|
||||
const res = await request.get<ApiResult<PageResult<ApprovalItem>>>(
|
||||
THINK_API_URL + '/approval/todo/page',
|
||||
{
|
||||
params
|
||||
}
|
||||
);
|
||||
if (res.data.code === 0 && res.data.data) {
|
||||
return res.data.data;
|
||||
}
|
||||
return Promise.reject(new Error(res.data.message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 审批单据详情
|
||||
*/
|
||||
export async function getApprovalInstance(id: number) {
|
||||
const res = await request.get<ApiResult<ApprovalInstance>>(
|
||||
THINK_API_URL + '/approval/instance/' + id
|
||||
);
|
||||
if (res.data.code === 0 && res.data.data) {
|
||||
return res.data.data;
|
||||
}
|
||||
return Promise.reject(new Error(res.data.message));
|
||||
}
|
||||
@@ -0,0 +1,188 @@
|
||||
import type { PageParam } from '@/api';
|
||||
|
||||
/**
|
||||
* 审批单据列表项
|
||||
*/
|
||||
export interface ApprovalItem {
|
||||
id?: number;
|
||||
// 单号
|
||||
bizNo?: string;
|
||||
// 单据类型:completion / coordination
|
||||
docType?: string;
|
||||
// 单据类型名称
|
||||
docTypeName?: string;
|
||||
// 状态
|
||||
status?: string;
|
||||
// 状态名称
|
||||
statusName?: string;
|
||||
// 标题或名称
|
||||
title?: string;
|
||||
// 提交时间
|
||||
submitTime?: string;
|
||||
// 当前节点名称
|
||||
currentNodeName?: string;
|
||||
// 发起人
|
||||
reporterId?: number;
|
||||
reporterName?: string;
|
||||
// 报送部门
|
||||
reportOrgName?: string;
|
||||
// 受理部门
|
||||
acceptOrgName?: string;
|
||||
// 归档时间
|
||||
archiveTime?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 待办/已办查询条件
|
||||
*/
|
||||
export interface ApprovalItemParam extends PageParam {
|
||||
status?: string;
|
||||
docType?: string;
|
||||
keywords?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 审批附件
|
||||
*/
|
||||
export interface ApprovalAttachment {
|
||||
id?: number;
|
||||
fileId?: number;
|
||||
name?: string;
|
||||
url?: string;
|
||||
length?: number;
|
||||
category?: string;
|
||||
categoryName?: string;
|
||||
uploaderId?: number;
|
||||
uploaderName?: string;
|
||||
createTime?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理记录
|
||||
*/
|
||||
export interface ApprovalRecord {
|
||||
id?: number;
|
||||
taskId?: number;
|
||||
nodeKey?: string;
|
||||
nodeName?: string;
|
||||
operatorId?: number;
|
||||
operatorName?: string;
|
||||
action?: string;
|
||||
actionName?: string;
|
||||
opinion?: string;
|
||||
round?: number;
|
||||
createTime?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 当前轮存档确认人
|
||||
*/
|
||||
export interface ApprovalConfirm {
|
||||
userId?: number;
|
||||
userName?: string;
|
||||
status?: string;
|
||||
statusName?: string;
|
||||
handleTime?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 当前用户的待办任务
|
||||
*/
|
||||
export interface ApprovalMyTask {
|
||||
id?: number;
|
||||
nodeKey?: string;
|
||||
nodeType?: string;
|
||||
status?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 详情动作权限
|
||||
*/
|
||||
export interface ApprovalPermissions {
|
||||
canEdit?: boolean;
|
||||
canSubmit?: boolean;
|
||||
canWithdraw?: boolean;
|
||||
canApprove?: boolean;
|
||||
canReject?: boolean;
|
||||
canConfirm?: boolean;
|
||||
canArchive?: boolean;
|
||||
canDispatch?: boolean;
|
||||
canAssign?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 审批单据详情
|
||||
*/
|
||||
export interface ApprovalInstance {
|
||||
id?: number;
|
||||
bizNo?: string;
|
||||
docType?: string;
|
||||
docTypeName?: string;
|
||||
flowCode?: string;
|
||||
status?: string;
|
||||
statusName?: string;
|
||||
round?: number;
|
||||
reporterId?: number;
|
||||
reporterName?: string;
|
||||
reportOrgId?: number;
|
||||
reportOrgName?: string;
|
||||
acceptOrgId?: number;
|
||||
acceptOrgName?: string;
|
||||
currentNodeKey?: string;
|
||||
currentNodeType?: string;
|
||||
currentNodeName?: string;
|
||||
submitTime?: string;
|
||||
approveTime?: string;
|
||||
archiveTime?: string;
|
||||
archivistId?: number;
|
||||
archivistName?: string;
|
||||
// 业务完成确认单字段
|
||||
name?: string;
|
||||
implementOrg?: string;
|
||||
fee?: number;
|
||||
serviceContent?: string;
|
||||
// 内部协调单字段
|
||||
title?: string;
|
||||
content?: string;
|
||||
remark?: string;
|
||||
dispatchTargetId?: number;
|
||||
dispatchTargetName?: string;
|
||||
attachments?: ApprovalAttachment[];
|
||||
records?: ApprovalRecord[];
|
||||
confirms?: ApprovalConfirm[];
|
||||
myTask?: ApprovalMyTask | null;
|
||||
permissions?: ApprovalPermissions;
|
||||
}
|
||||
|
||||
/**
|
||||
* 改派候选用户
|
||||
*/
|
||||
export interface ApprovalUser {
|
||||
userId?: number;
|
||||
username?: string;
|
||||
nickname?: string;
|
||||
realName?: string;
|
||||
organizationId?: number;
|
||||
organizationName?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 改派参数
|
||||
*/
|
||||
export interface ApprovalAssignParam {
|
||||
targetUserId: number;
|
||||
reason: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 改派结果
|
||||
*/
|
||||
export interface ApprovalAssignResult {
|
||||
instanceId?: number;
|
||||
taskId?: number;
|
||||
originalAssigneeId?: number;
|
||||
originalAssigneeName?: string;
|
||||
assigneeId?: number;
|
||||
assigneeName?: string;
|
||||
reason?: string;
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
import request from '@/utils/request';
|
||||
import type { ApiResult } from '@/api';
|
||||
import { THINK_API_URL } from '@/config/setting';
|
||||
import type {
|
||||
ApprovalAssignParam,
|
||||
ApprovalAssignResult,
|
||||
ApprovalUser
|
||||
} from '../model';
|
||||
|
||||
/**
|
||||
* 通过任务
|
||||
*/
|
||||
export async function approveApprovalTask(id: number, opinion?: string) {
|
||||
const res = await request.post<ApiResult<unknown>>(
|
||||
THINK_API_URL + '/approval/task/' + id + '/approve',
|
||||
{ opinion }
|
||||
);
|
||||
if (res.data.code === 0) {
|
||||
return res.data.message;
|
||||
}
|
||||
return Promise.reject(new Error(res.data.message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 驳回任务(意见必填)
|
||||
*/
|
||||
export async function rejectApprovalTask(id: number, opinion: string) {
|
||||
const res = await request.post<ApiResult<unknown>>(
|
||||
THINK_API_URL + '/approval/task/' + id + '/reject',
|
||||
{ opinion }
|
||||
);
|
||||
if (res.data.code === 0) {
|
||||
return res.data.message;
|
||||
}
|
||||
return Promise.reject(new Error(res.data.message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 存档确认
|
||||
*/
|
||||
export async function confirmApprovalTask(id: number) {
|
||||
const res = await request.post<ApiResult<unknown>>(
|
||||
THINK_API_URL + '/approval/task/' + id + '/confirm'
|
||||
);
|
||||
if (res.data.code === 0) {
|
||||
return res.data.message;
|
||||
}
|
||||
return Promise.reject(new Error(res.data.message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 归档
|
||||
*/
|
||||
export async function archiveApprovalTask(id: number) {
|
||||
const res = await request.post<ApiResult<unknown>>(
|
||||
THINK_API_URL + '/approval/task/' + id + '/archive'
|
||||
);
|
||||
if (res.data.code === 0) {
|
||||
return res.data.message;
|
||||
}
|
||||
return Promise.reject(new Error(res.data.message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 改派待办
|
||||
*/
|
||||
export async function assignApprovalTask(
|
||||
id: number,
|
||||
data: ApprovalAssignParam
|
||||
) {
|
||||
const res = await request.post<ApiResult<ApprovalAssignResult>>(
|
||||
THINK_API_URL + '/approval/task/' + id + '/assign',
|
||||
data
|
||||
);
|
||||
if (res.data.code === 0) {
|
||||
return res.data.data;
|
||||
}
|
||||
return Promise.reject(new Error(res.data.message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 改派候选用户:同租户启用用户,最多 50 条
|
||||
*/
|
||||
export async function searchApprovalUsers(keywords?: string) {
|
||||
const res = await request.get<ApiResult<ApprovalUser[]>>(
|
||||
THINK_API_URL + '/approval/directory/users',
|
||||
{
|
||||
params: { keywords }
|
||||
}
|
||||
);
|
||||
if (res.data.code === 0 && res.data.data) {
|
||||
return res.data.data;
|
||||
}
|
||||
return Promise.reject(new Error(res.data.message));
|
||||
}
|
||||
@@ -0,0 +1,559 @@
|
||||
<!-- 审批待办详情抽屉 -->
|
||||
<template>
|
||||
<a-drawer
|
||||
:width="860"
|
||||
:visible="visible"
|
||||
title="待办详情"
|
||||
:body-style="{ padding: '16px' }"
|
||||
:footer="null"
|
||||
@update:visible="updateVisible"
|
||||
>
|
||||
<a-spin :spinning="loading">
|
||||
<template v-if="detail">
|
||||
<!-- 动作按钮:按详情权限与按钮权限显隐 -->
|
||||
<div class="approval-detail-actions">
|
||||
<a-space>
|
||||
<a-button
|
||||
v-if="detail.permissions?.canApprove"
|
||||
v-permission="'approval:instance:approve'"
|
||||
type="primary"
|
||||
@click="openOpinion('approve')"
|
||||
>
|
||||
通过
|
||||
</a-button>
|
||||
<a-button
|
||||
v-if="detail.permissions?.canReject"
|
||||
v-permission="'approval:instance:approve'"
|
||||
danger
|
||||
@click="openOpinion('reject')"
|
||||
>
|
||||
驳回
|
||||
</a-button>
|
||||
<a-button
|
||||
v-if="detail.permissions?.canConfirm"
|
||||
v-permission="'approval:instance:approve'"
|
||||
type="primary"
|
||||
@click="confirmStore"
|
||||
>
|
||||
存档
|
||||
</a-button>
|
||||
<a-button
|
||||
v-if="detail.permissions?.canArchive"
|
||||
v-permission="'approval:instance:archive'"
|
||||
type="primary"
|
||||
@click="archive"
|
||||
>
|
||||
归档
|
||||
</a-button>
|
||||
<a-button
|
||||
v-if="detail.permissions?.canAssign"
|
||||
v-permission="'approval:instance:assign'"
|
||||
@click="openAssign"
|
||||
>
|
||||
改派
|
||||
</a-button>
|
||||
</a-space>
|
||||
<a-tag v-if="detail.statusName" :color="statusColor(detail.status)">
|
||||
{{ detail.statusName }}
|
||||
</a-tag>
|
||||
</div>
|
||||
|
||||
<a-descriptions bordered size="small" :column="2" title="基本信息">
|
||||
<a-descriptions-item label="单号">
|
||||
{{ detail.bizNo }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="单据类型">
|
||||
{{ detail.docTypeName }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="当前节点">
|
||||
{{ detail.currentNodeName || '-' }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="发起人">
|
||||
{{ detail.reporterName }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="报送部门">
|
||||
{{ detail.reportOrgName }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item
|
||||
v-if="detail.docType === 'coordination'"
|
||||
label="受理部门"
|
||||
>
|
||||
{{ detail.acceptOrgName }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="提交时间">
|
||||
{{ formatApprovalTime(detail.submitTime) }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="审批完成时间">
|
||||
{{ formatApprovalTime(detail.approveTime) }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="归档时间">
|
||||
{{ formatApprovalTime(detail.archiveTime) }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="归档人">
|
||||
{{ detail.archivistName }}
|
||||
</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
|
||||
<a-descriptions
|
||||
v-if="detail.docType === 'completion'"
|
||||
bordered
|
||||
size="small"
|
||||
:column="2"
|
||||
title="单据字段"
|
||||
>
|
||||
<a-descriptions-item label="名称" :span="2">
|
||||
{{ detail.name }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="实施单位">
|
||||
{{ detail.implementOrg }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="费用(元)">
|
||||
{{ formatFee(detail.fee) }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="服务内容" :span="2">
|
||||
{{ detail.serviceContent }}
|
||||
</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
|
||||
<a-descriptions
|
||||
v-else-if="detail.docType === 'coordination'"
|
||||
bordered
|
||||
size="small"
|
||||
:column="2"
|
||||
title="单据字段"
|
||||
>
|
||||
<a-descriptions-item label="标题" :span="2">
|
||||
{{ detail.title }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="协调内容" :span="2">
|
||||
{{ detail.content }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="备注" :span="2">
|
||||
{{ detail.remark }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="派单对象">
|
||||
{{ detail.dispatchTargetName || '-' }}
|
||||
</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
|
||||
<a-descriptions bordered size="small" :column="2" title="附件">
|
||||
<a-descriptions-item label="附件" :span="2">
|
||||
<a-empty
|
||||
v-if="!attachments.length"
|
||||
:image="Empty.PRESENTED_IMAGE_SIMPLE"
|
||||
description="暂无附件"
|
||||
/>
|
||||
<div
|
||||
v-for="file in attachments"
|
||||
:key="file.id"
|
||||
class="approval-file"
|
||||
>
|
||||
<a @click="openFile(file)">{{ file.name }}</a>
|
||||
<span class="ele-text-secondary">
|
||||
{{ file.categoryName }} · {{ formatFileSize(file.length) }} ·
|
||||
{{ formatApprovalTime(file.createTime) }}
|
||||
</span>
|
||||
</div>
|
||||
</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
|
||||
<a-descriptions bordered size="small" :column="2" title="当前节点">
|
||||
<a-descriptions-item label="节点">
|
||||
{{ detail.currentNodeName || '-' }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="存档确认">
|
||||
<template v-if="confirms.length">
|
||||
<a-tag
|
||||
v-for="item in confirms"
|
||||
:key="item.userId"
|
||||
:color="item.status === 'DONE' ? 'green' : 'blue'"
|
||||
>
|
||||
{{ item.userName }}·{{ item.statusName }}
|
||||
</a-tag>
|
||||
</template>
|
||||
<span v-else>-</span>
|
||||
</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
|
||||
<div class="approval-section-title">处理记录</div>
|
||||
<a-empty
|
||||
v-if="!records.length"
|
||||
:image="Empty.PRESENTED_IMAGE_SIMPLE"
|
||||
description="暂无处理记录"
|
||||
/>
|
||||
<a-timeline v-else>
|
||||
<a-timeline-item
|
||||
v-for="item in records"
|
||||
:key="item.id"
|
||||
:color="item.action === 'REJECT' ? 'red' : 'blue'"
|
||||
>
|
||||
<div>
|
||||
<span class="approval-timeline-action">{{
|
||||
item.actionName
|
||||
}}</span>
|
||||
<span class="ele-text-secondary">
|
||||
{{ item.operatorName }} ·
|
||||
{{ formatApprovalTime(item.createTime) }}
|
||||
</span>
|
||||
</div>
|
||||
<div v-if="item.nodeName" class="ele-text-secondary">
|
||||
{{ item.nodeName }}
|
||||
</div>
|
||||
<div v-if="item.opinion">{{ item.opinion }}</div>
|
||||
</a-timeline-item>
|
||||
</a-timeline>
|
||||
</template>
|
||||
<a-empty v-else-if="!loading" description="暂无数据" />
|
||||
</a-spin>
|
||||
|
||||
<!-- 通过/驳回意见 -->
|
||||
<a-modal
|
||||
v-model:visible="opinionVisible"
|
||||
:title="opinionMode === 'approve' ? '通过' : '驳回'"
|
||||
:confirm-loading="submitting"
|
||||
:mask-closable="false"
|
||||
@ok="submitOpinion"
|
||||
>
|
||||
<a-textarea
|
||||
v-model:value="opinion"
|
||||
:rows="4"
|
||||
:maxlength="500"
|
||||
show-count
|
||||
:placeholder="
|
||||
opinionMode === 'approve'
|
||||
? '请输入意见(可选)'
|
||||
: '请输入驳回意见(必填)'
|
||||
"
|
||||
/>
|
||||
</a-modal>
|
||||
|
||||
<!-- 改派 -->
|
||||
<a-modal
|
||||
v-model:visible="assignVisible"
|
||||
title="改派待办"
|
||||
:confirm-loading="submitting"
|
||||
:mask-closable="false"
|
||||
@ok="submitAssign"
|
||||
>
|
||||
<a-form layout="vertical">
|
||||
<a-form-item label="改派对象" required>
|
||||
<a-select
|
||||
v-model:value="assignForm.targetUserId"
|
||||
show-search
|
||||
allow-clear
|
||||
:filter-option="false"
|
||||
:options="userOptions"
|
||||
:loading="userLoading"
|
||||
placeholder="搜索姓名或账号"
|
||||
@search="searchUsers"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="改派原因" required>
|
||||
<a-textarea
|
||||
v-model:value="assignForm.reason"
|
||||
:rows="3"
|
||||
:maxlength="500"
|
||||
show-count
|
||||
placeholder="请输入改派原因"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
</a-drawer>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, reactive, ref, watch } from 'vue';
|
||||
import { Empty, Modal, message } from 'ant-design-vue';
|
||||
import type {
|
||||
ApprovalAttachment,
|
||||
ApprovalInstance
|
||||
} from '@/api/approval/model';
|
||||
import { getApprovalInstance } from '@/api/approval/instance';
|
||||
import {
|
||||
approveApprovalTask,
|
||||
archiveApprovalTask,
|
||||
assignApprovalTask,
|
||||
confirmApprovalTask,
|
||||
rejectApprovalTask,
|
||||
searchApprovalUsers
|
||||
} from '@/api/approval/task';
|
||||
import {
|
||||
approvalStatusColor,
|
||||
formatApprovalTime,
|
||||
formatFileSize
|
||||
} from '../format';
|
||||
|
||||
const props = defineProps<{
|
||||
// 抽屉是否打开
|
||||
visible: boolean;
|
||||
// 单据id
|
||||
instanceId?: number | null;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:visible', visible: boolean): void;
|
||||
// 单据状态已变化,需要刷新列表
|
||||
(e: 'done'): void;
|
||||
}>();
|
||||
|
||||
// 详情数据
|
||||
const detail = ref<ApprovalInstance | null>(null);
|
||||
// 加载状态
|
||||
const loading = ref(false);
|
||||
// 提交状态
|
||||
const submitting = ref(false);
|
||||
|
||||
const attachments = computed<ApprovalAttachment[]>(
|
||||
() => detail.value?.attachments ?? []
|
||||
);
|
||||
const records = computed(() => detail.value?.records ?? []);
|
||||
const confirms = computed(() => detail.value?.confirms ?? []);
|
||||
|
||||
const load = async () => {
|
||||
if (!props.instanceId) {
|
||||
detail.value = null;
|
||||
return;
|
||||
}
|
||||
loading.value = true;
|
||||
try {
|
||||
detail.value = await getApprovalInstance(props.instanceId);
|
||||
} catch (e: any) {
|
||||
detail.value = null;
|
||||
message.error(e.message);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
watch(
|
||||
() => [props.visible, props.instanceId],
|
||||
() => {
|
||||
if (props.visible && props.instanceId) {
|
||||
load();
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
const updateVisible = (value: boolean) => {
|
||||
emit('update:visible', value);
|
||||
};
|
||||
|
||||
// 状态颜色
|
||||
const statusColor = approvalStatusColor;
|
||||
|
||||
const formatFee = (fee?: number) => {
|
||||
return fee === undefined || fee === null ? '-' : Number(fee).toFixed(2);
|
||||
};
|
||||
|
||||
const openFile = (file: ApprovalAttachment) => {
|
||||
if (file.url) {
|
||||
window.open(file.url);
|
||||
}
|
||||
};
|
||||
|
||||
/* 通过 / 驳回 */
|
||||
const opinionVisible = ref(false);
|
||||
const opinionMode = ref<'approve' | 'reject'>('approve');
|
||||
const opinion = ref('');
|
||||
|
||||
const openOpinion = (mode: 'approve' | 'reject') => {
|
||||
opinionMode.value = mode;
|
||||
opinion.value = '';
|
||||
opinionVisible.value = true;
|
||||
};
|
||||
|
||||
const submitOpinion = async () => {
|
||||
const taskId = detail.value?.myTask?.id;
|
||||
if (!taskId) {
|
||||
message.error('待办任务不存在,请刷新');
|
||||
return;
|
||||
}
|
||||
const text = opinion.value.trim();
|
||||
if (opinionMode.value === 'reject' && !text) {
|
||||
message.error('请填写驳回意见');
|
||||
return;
|
||||
}
|
||||
submitting.value = true;
|
||||
try {
|
||||
if (opinionMode.value === 'approve') {
|
||||
const msg = await approveApprovalTask(taskId, text || undefined);
|
||||
if (msg && msg !== '审批成功') {
|
||||
message.warning(msg);
|
||||
} else {
|
||||
message.success('审批成功');
|
||||
}
|
||||
} else {
|
||||
await rejectApprovalTask(taskId, text);
|
||||
message.success('驳回成功');
|
||||
}
|
||||
opinionVisible.value = false;
|
||||
await load();
|
||||
emit('done');
|
||||
} catch (e: any) {
|
||||
message.error(e.message);
|
||||
} finally {
|
||||
submitting.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
/* 存档 / 归档 */
|
||||
const confirmStore = () => {
|
||||
const taskId = detail.value?.myTask?.id;
|
||||
if (!taskId) {
|
||||
message.error('待办任务不存在,请刷新');
|
||||
return;
|
||||
}
|
||||
Modal.confirm({
|
||||
title: '提示',
|
||||
content: '确认完成存档?',
|
||||
onOk: async () => {
|
||||
try {
|
||||
await confirmApprovalTask(taskId);
|
||||
message.success('存档成功');
|
||||
await load();
|
||||
emit('done');
|
||||
} catch (e: any) {
|
||||
message.error(e.message);
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const archive = () => {
|
||||
const taskId = detail.value?.myTask?.id;
|
||||
if (!taskId) {
|
||||
message.error('待办任务不存在,请刷新');
|
||||
return;
|
||||
}
|
||||
Modal.confirm({
|
||||
title: '提示',
|
||||
content: '确认归档该单据?归档后不可再修改。',
|
||||
onOk: async () => {
|
||||
try {
|
||||
await archiveApprovalTask(taskId);
|
||||
message.success('归档成功');
|
||||
await load();
|
||||
emit('done');
|
||||
} catch (e: any) {
|
||||
message.error(e.message);
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/* 改派 */
|
||||
const assignVisible = ref(false);
|
||||
const userLoading = ref(false);
|
||||
const userOptions = ref<{ label: string; value: number }[]>([]);
|
||||
const assignForm = reactive<{ targetUserId?: number; reason: string }>({
|
||||
targetUserId: undefined,
|
||||
reason: ''
|
||||
});
|
||||
let searchTimer: ReturnType<typeof setTimeout> | undefined;
|
||||
|
||||
const doSearchUsers = async (keywords?: string) => {
|
||||
userLoading.value = true;
|
||||
try {
|
||||
const users = await searchApprovalUsers(keywords?.trim() || undefined);
|
||||
userOptions.value = users.map((user) => ({
|
||||
value: user.userId as number,
|
||||
label:
|
||||
(user.realName || user.nickname || user.username || '') +
|
||||
(user.organizationName ? '(' + user.organizationName + ')' : '')
|
||||
}));
|
||||
} catch (e: any) {
|
||||
message.error(e.message);
|
||||
} finally {
|
||||
userLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const searchUsers = (keywords?: string) => {
|
||||
if (searchTimer) {
|
||||
clearTimeout(searchTimer);
|
||||
}
|
||||
searchTimer = setTimeout(() => doSearchUsers(keywords), 300);
|
||||
};
|
||||
|
||||
const openAssign = () => {
|
||||
assignForm.targetUserId = undefined;
|
||||
assignForm.reason = '';
|
||||
assignVisible.value = true;
|
||||
doSearchUsers();
|
||||
};
|
||||
|
||||
const submitAssign = async () => {
|
||||
const taskId = detail.value?.myTask?.id;
|
||||
if (!taskId) {
|
||||
message.error('待办任务不存在,请刷新');
|
||||
return;
|
||||
}
|
||||
if (!assignForm.targetUserId) {
|
||||
message.error('请选择改派对象');
|
||||
return;
|
||||
}
|
||||
const reason = assignForm.reason.trim();
|
||||
if (!reason) {
|
||||
message.error('请填写改派原因');
|
||||
return;
|
||||
}
|
||||
submitting.value = true;
|
||||
try {
|
||||
await assignApprovalTask(taskId, {
|
||||
targetUserId: assignForm.targetUserId,
|
||||
reason
|
||||
});
|
||||
message.success('改派成功');
|
||||
assignVisible.value = false;
|
||||
emit('done');
|
||||
// 改派后待办不再属于当前用户,关闭抽屉
|
||||
updateVisible(false);
|
||||
} catch (e: any) {
|
||||
message.error(e.message);
|
||||
} finally {
|
||||
submitting.value = false;
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'ApprovalTodoDetail'
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.approval-detail-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.approval-section-title {
|
||||
margin: 24px 0 16px;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.approval-file {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-bottom: 8px;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.approval-timeline-action {
|
||||
margin-right: 8px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
:deep(.ant-descriptions) {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,46 @@
|
||||
import { toDateString } from 'ele-admin-pro';
|
||||
|
||||
/**
|
||||
* 审批时间统一展示:兼容 ISO 字符串与时间戳
|
||||
*/
|
||||
export function formatApprovalTime(value?: string | number | null): string {
|
||||
if (value === undefined || value === null || value === '') {
|
||||
return '';
|
||||
}
|
||||
if (typeof value === 'number') {
|
||||
return toDateString(value, 'yyyy-MM-dd HH:mm:ss');
|
||||
}
|
||||
return String(value).replace('T', ' ').slice(0, 19);
|
||||
}
|
||||
|
||||
/**
|
||||
* 附件大小展示
|
||||
*/
|
||||
export function formatFileSize(length?: number): string {
|
||||
if (!length || length <= 0) {
|
||||
return '0 KB';
|
||||
}
|
||||
if (length < 1024 * 1024) {
|
||||
return (length / 1024).toFixed(1) + ' KB';
|
||||
}
|
||||
return (length / 1024 / 1024).toFixed(1) + ' MB';
|
||||
}
|
||||
|
||||
/**
|
||||
* 单据状态标签颜色
|
||||
*/
|
||||
export function approvalStatusColor(status?: string): string {
|
||||
if (status === 'PENDING' || status === 'DRAFT') {
|
||||
return 'blue';
|
||||
}
|
||||
if (status === 'REJECTED') {
|
||||
return 'red';
|
||||
}
|
||||
if (status === 'WITHDRAWN') {
|
||||
return 'orange';
|
||||
}
|
||||
if (status === 'APPROVED') {
|
||||
return 'green';
|
||||
}
|
||||
return 'default';
|
||||
}
|
||||
@@ -0,0 +1,215 @@
|
||||
<!-- 审批管理 - 待办审批 -->
|
||||
<template>
|
||||
<div class="page">
|
||||
<div class="ele-body">
|
||||
<a-card :bordered="false" :body-style="{ padding: '16px' }">
|
||||
<ele-pro-table
|
||||
ref="tableRef"
|
||||
row-key="id"
|
||||
:columns="columns"
|
||||
:datasource="datasource"
|
||||
:customRow="customRow"
|
||||
tool-class="ele-toolbar-form"
|
||||
class="approval-todo-table"
|
||||
:scroll="{ x: 1200 }"
|
||||
>
|
||||
<template #toolbar>
|
||||
<a-space :size="10" style="flex-wrap: wrap">
|
||||
<a-select
|
||||
v-model:value="where.status"
|
||||
allow-clear
|
||||
placeholder="单据状态"
|
||||
style="width: 160px"
|
||||
:options="statusOptions"
|
||||
@change="reload"
|
||||
/>
|
||||
<a-input-search
|
||||
v-model:value="where.keywords"
|
||||
allow-clear
|
||||
placeholder="请输入单号/标题/实施单位"
|
||||
style="width: 320px"
|
||||
@pressEnter="reload"
|
||||
@search="reload"
|
||||
/>
|
||||
<a-button @click="resetSearch">重置</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'status'">
|
||||
<a-tag :color="approvalStatusColor(record.status)">
|
||||
{{ record.statusName }}
|
||||
</a-tag>
|
||||
</template>
|
||||
<template v-if="column.key === 'submitTime'">
|
||||
{{ formatApprovalTime(record.submitTime) }}
|
||||
</template>
|
||||
<template v-if="column.key === 'action'">
|
||||
<a @click="openDetail(record)">详情</a>
|
||||
</template>
|
||||
</template>
|
||||
</ele-pro-table>
|
||||
</a-card>
|
||||
|
||||
<!-- 详情抽屉 -->
|
||||
<detail-drawer
|
||||
v-model:visible="showDetail"
|
||||
:instance-id="currentId"
|
||||
@done="reload"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue';
|
||||
import type { EleProTable } from 'ele-admin-pro';
|
||||
import type {
|
||||
DatasourceFunction,
|
||||
ColumnItem
|
||||
} from 'ele-admin-pro/es/ele-pro-table/types';
|
||||
import useSearch from '@/utils/use-search';
|
||||
import type { ApprovalItem, ApprovalItemParam } from '@/api/approval/model';
|
||||
import { pageApprovalTodo } from '@/api/approval/instance';
|
||||
import { approvalStatusColor, formatApprovalTime } from './format';
|
||||
import DetailDrawer from './components/detail-drawer.vue';
|
||||
|
||||
// 表格实例
|
||||
const tableRef = ref<InstanceType<typeof EleProTable> | null>(null);
|
||||
// 详情抽屉
|
||||
const showDetail = ref(false);
|
||||
// 当前单据id
|
||||
const currentId = ref<number | null>(null);
|
||||
|
||||
// 搜索条件
|
||||
const { where, resetFields } = useSearch<ApprovalItemParam>({
|
||||
status: undefined,
|
||||
keywords: undefined
|
||||
});
|
||||
|
||||
// 状态选项
|
||||
const statusOptions = [
|
||||
{ label: '审批中', value: 'PENDING' },
|
||||
{ label: '已驳回', value: 'REJECTED' },
|
||||
{ label: '已撤回', value: 'WITHDRAWN' },
|
||||
{ label: '审批完成', value: 'APPROVED' },
|
||||
{ label: '已归档', value: 'ARCHIVED' }
|
||||
];
|
||||
|
||||
// 表格列配置
|
||||
const columns = ref<ColumnItem[]>([
|
||||
{
|
||||
title: '单号',
|
||||
dataIndex: 'bizNo',
|
||||
key: 'bizNo',
|
||||
width: 150,
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
title: '单据类型',
|
||||
dataIndex: 'docTypeName',
|
||||
key: 'docTypeName',
|
||||
width: 140,
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
title: '标题或名称',
|
||||
dataIndex: 'title',
|
||||
key: 'title',
|
||||
minWidth: 200,
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
title: '发起人',
|
||||
dataIndex: 'reporterName',
|
||||
key: 'reporterName',
|
||||
width: 120,
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
title: '报送部门',
|
||||
dataIndex: 'reportOrgName',
|
||||
key: 'reportOrgName',
|
||||
width: 160,
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'statusName',
|
||||
key: 'status',
|
||||
width: 110,
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
title: '提交时间',
|
||||
dataIndex: 'submitTime',
|
||||
key: 'submitTime',
|
||||
width: 170,
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
title: '当前节点',
|
||||
dataIndex: 'currentNodeName',
|
||||
key: 'currentNodeName',
|
||||
width: 140,
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
width: 90,
|
||||
fixed: 'right',
|
||||
align: 'center',
|
||||
hideInSetting: true
|
||||
}
|
||||
]);
|
||||
|
||||
// 表格数据源
|
||||
const datasource: DatasourceFunction = ({ page, limit }) => {
|
||||
return pageApprovalTodo({
|
||||
status: where.status,
|
||||
keywords: where.keywords?.trim() || undefined,
|
||||
page,
|
||||
limit
|
||||
});
|
||||
};
|
||||
|
||||
// 重新加载表格
|
||||
const reload = () => {
|
||||
tableRef.value?.reload({ page: 1 });
|
||||
};
|
||||
|
||||
// 重置搜索
|
||||
const resetSearch = () => {
|
||||
resetFields();
|
||||
reload();
|
||||
};
|
||||
|
||||
// 打开详情
|
||||
const openDetail = (row: ApprovalItem) => {
|
||||
currentId.value = row.id ?? null;
|
||||
showDetail.value = true;
|
||||
};
|
||||
|
||||
// 行双击打开详情
|
||||
const customRow = (record: ApprovalItem) => {
|
||||
return {
|
||||
onDblclick: () => {
|
||||
openDetail(record);
|
||||
}
|
||||
};
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'ApprovalTodo'
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.approval-todo-table {
|
||||
:deep(.ant-table-cell) {
|
||||
vertical-align: middle;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user