merge: 一期线下审批(#18-#26)
This commit is contained in:
@@ -0,0 +1,34 @@
|
|||||||
|
import request from '@/utils/request';
|
||||||
|
import type { ApiResult } from '@/api';
|
||||||
|
import type { ApprovalOrganization, ApprovalUser } from '../instance/model';
|
||||||
|
import { THINK_API_URL } from '@/config/setting';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 启用组织(审批管理下拉)
|
||||||
|
*/
|
||||||
|
export async function listApprovalOrganizations() {
|
||||||
|
const res = await request.get<ApiResult<ApprovalOrganization[]>>(
|
||||||
|
THINK_API_URL + '/approval/directory/organizations'
|
||||||
|
);
|
||||||
|
if (res.data.code === 0 && res.data.data) {
|
||||||
|
return res.data.data;
|
||||||
|
}
|
||||||
|
return Promise.reject(new Error(res.data.message));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 启用用户远程搜索(审批管理下拉)
|
||||||
|
*/
|
||||||
|
export async function searchApprovalUsers(params?: {
|
||||||
|
keywords?: string;
|
||||||
|
organizationId?: number;
|
||||||
|
}) {
|
||||||
|
const res = await request.get<ApiResult<ApprovalUser[]>>(
|
||||||
|
THINK_API_URL + '/approval/directory/users',
|
||||||
|
{ params }
|
||||||
|
);
|
||||||
|
if (res.data.code === 0 && res.data.data) {
|
||||||
|
return res.data.data;
|
||||||
|
}
|
||||||
|
return Promise.reject(new Error(res.data.message));
|
||||||
|
}
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
import request from '@/utils/request';
|
||||||
|
import type { ApiResult, PageResult } from '@/api';
|
||||||
|
import type {
|
||||||
|
ApprovalExportFile,
|
||||||
|
ApprovalInstance,
|
||||||
|
ApprovalItem,
|
||||||
|
ApprovalQueryParam,
|
||||||
|
ApprovalTodoParam
|
||||||
|
} from './model';
|
||||||
|
import { THINK_API_URL } from '@/config/setting';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 单据查询分页
|
||||||
|
*/
|
||||||
|
export async function pageInstances(params: ApprovalQueryParam) {
|
||||||
|
const res = await request.get<ApiResult<PageResult<ApprovalItem>>>(
|
||||||
|
THINK_API_URL + '/approval/instance/page',
|
||||||
|
{ params }
|
||||||
|
);
|
||||||
|
if (res.data.code === 0) {
|
||||||
|
return res.data.data;
|
||||||
|
}
|
||||||
|
return Promise.reject(new Error(res.data.message));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 我的待办分页
|
||||||
|
*/
|
||||||
|
export async function pageApprovalTodo(params: ApprovalTodoParam) {
|
||||||
|
const res = await request.get<ApiResult<PageResult<ApprovalItem>>>(
|
||||||
|
THINK_API_URL + '/approval/todo/page',
|
||||||
|
{ params }
|
||||||
|
);
|
||||||
|
if (res.data.code === 0) {
|
||||||
|
return res.data.data;
|
||||||
|
}
|
||||||
|
return Promise.reject(new Error(res.data.message));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 单据详情(只读)
|
||||||
|
*/
|
||||||
|
export async function getInstance(id: number) {
|
||||||
|
const res = await request.get<ApiResult<ApprovalInstance>>(
|
||||||
|
THINK_API_URL + '/approval/instance/' + id
|
||||||
|
);
|
||||||
|
if (res.data.code === 0) {
|
||||||
|
return res.data.data;
|
||||||
|
}
|
||||||
|
return Promise.reject(new Error(res.data.message));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 导出单据(返回完整结果,含文件下载地址)
|
||||||
|
*/
|
||||||
|
export async function exportInstances(data: ApprovalQueryParam) {
|
||||||
|
const res = await request.post<ApiResult<ApprovalExportFile>>(
|
||||||
|
THINK_API_URL + '/approval/export',
|
||||||
|
data
|
||||||
|
);
|
||||||
|
if (res.data.code === 0) {
|
||||||
|
return res.data;
|
||||||
|
}
|
||||||
|
return Promise.reject(new Error(res.data.message));
|
||||||
|
}
|
||||||
@@ -0,0 +1,234 @@
|
|||||||
|
import type { PageParam } from '@/api';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 单据查询条件
|
||||||
|
*/
|
||||||
|
export interface ApprovalQueryParam extends PageParam {
|
||||||
|
// 单据类型 completion/coordination
|
||||||
|
docType?: string;
|
||||||
|
// 单号
|
||||||
|
bizNo?: string;
|
||||||
|
// 关键词:单号、名称或标题、实施单位
|
||||||
|
keywords?: string;
|
||||||
|
// 报送部门id
|
||||||
|
reportOrgId?: number;
|
||||||
|
// 受理部门id
|
||||||
|
acceptOrgId?: number;
|
||||||
|
// 报送人id
|
||||||
|
reporterId?: number;
|
||||||
|
// 单据状态
|
||||||
|
status?: string;
|
||||||
|
// 提交时间起
|
||||||
|
submitTimeStart?: string;
|
||||||
|
// 提交时间止
|
||||||
|
submitTimeEnd?: string;
|
||||||
|
// 归档时间起
|
||||||
|
archiveTimeStart?: string;
|
||||||
|
// 归档时间止
|
||||||
|
archiveTimeEnd?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 单据列表行
|
||||||
|
*/
|
||||||
|
export interface ApprovalItem {
|
||||||
|
id?: number;
|
||||||
|
bizNo?: string;
|
||||||
|
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 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 实例上的待办任务(PC 按任务改派)
|
||||||
|
*/
|
||||||
|
export interface ApprovalPendingTask {
|
||||||
|
id?: number;
|
||||||
|
nodeKey?: string;
|
||||||
|
nodeName?: string;
|
||||||
|
nodeType?: string;
|
||||||
|
assigneeId?: number;
|
||||||
|
assigneeName?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 我的待处理任务
|
||||||
|
*/
|
||||||
|
export interface ApprovalMyTask {
|
||||||
|
id?: number;
|
||||||
|
nodeKey?: string;
|
||||||
|
nodeType?: string;
|
||||||
|
status?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 单据详情
|
||||||
|
*/
|
||||||
|
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;
|
||||||
|
pendingTasks?: ApprovalPendingTask[];
|
||||||
|
permissions?: Record<string, boolean>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 审批目录组织
|
||||||
|
*/
|
||||||
|
export interface ApprovalOrganization {
|
||||||
|
organizationId?: number;
|
||||||
|
parentId?: number;
|
||||||
|
organizationName?: string;
|
||||||
|
organizationFullName?: string;
|
||||||
|
organizationCode?: string;
|
||||||
|
leaderId?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 审批目录用户
|
||||||
|
*/
|
||||||
|
export interface ApprovalUser {
|
||||||
|
userId?: number;
|
||||||
|
username?: string;
|
||||||
|
nickname?: string;
|
||||||
|
realName?: string;
|
||||||
|
organizationId?: number;
|
||||||
|
organizationName?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 导出文件
|
||||||
|
*/
|
||||||
|
export interface ApprovalExportFile {
|
||||||
|
id?: number;
|
||||||
|
name?: string;
|
||||||
|
path?: string;
|
||||||
|
length?: number;
|
||||||
|
contentType?: string;
|
||||||
|
url?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 待办/已办查询条件
|
||||||
|
*/
|
||||||
|
export interface ApprovalTodoParam extends PageParam {
|
||||||
|
// 单据状态
|
||||||
|
status?: string;
|
||||||
|
// 单据类型
|
||||||
|
docType?: string;
|
||||||
|
// 查询范围:mine(默认本人待办)/all(全租户待办,需 assign 权限)
|
||||||
|
scope?: 'mine' | 'all';
|
||||||
|
// 关键词:单号、名称或标题、实施单位
|
||||||
|
keywords?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 待办改派参数
|
||||||
|
*/
|
||||||
|
export interface ApprovalAssignParam {
|
||||||
|
// 改派对象用户id
|
||||||
|
targetUserId: number;
|
||||||
|
// 改派原因
|
||||||
|
reason: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 改派结果
|
||||||
|
*/
|
||||||
|
export interface ApprovalAssignResult {
|
||||||
|
instanceId?: number;
|
||||||
|
taskId?: number;
|
||||||
|
originalAssigneeId?: number;
|
||||||
|
originalAssigneeName?: string;
|
||||||
|
assigneeId?: number;
|
||||||
|
assigneeName?: string;
|
||||||
|
reason?: string;
|
||||||
|
}
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
import request from '@/utils/request';
|
||||||
|
import type { ApiResult } from '@/api';
|
||||||
|
import { THINK_API_URL } from '@/config/setting';
|
||||||
|
import type {
|
||||||
|
ApprovalAssignParam,
|
||||||
|
ApprovalAssignResult,
|
||||||
|
ApprovalUser
|
||||||
|
} from '../instance/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 listDispatchCandidates(id: number) {
|
||||||
|
const res = await request.get<ApiResult<ApprovalUser[]>>(
|
||||||
|
THINK_API_URL + '/approval/task/' + id + '/candidates'
|
||||||
|
);
|
||||||
|
if (res.data.code === 0) {
|
||||||
|
return res.data.data ?? [];
|
||||||
|
}
|
||||||
|
return Promise.reject(new Error(res.data.message));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 派单(选定 1 人即完成派单节点)
|
||||||
|
*/
|
||||||
|
export async function dispatchApprovalTask(id: number, targetUserId: number) {
|
||||||
|
const res = await request.post<ApiResult<unknown>>(
|
||||||
|
THINK_API_URL + '/approval/task/' + id + '/dispatch',
|
||||||
|
{ targetUserId }
|
||||||
|
);
|
||||||
|
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));
|
||||||
|
}
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
import request from '@/utils/request';
|
import request from '@/utils/request';
|
||||||
import type { ApiResult, PageResult } from '@/api';
|
import type { ApiResult, PageResult } from '@/api';
|
||||||
import type { Role, RoleParam } from './model';
|
import type { Role, RoleMember, RoleMemberParam, RoleParam } from './model';
|
||||||
import type { Menu } from '../menu/model';
|
import type { Menu } from '../menu/model';
|
||||||
import { SERVER_API_URL } from '@/config/setting';
|
import { SERVER_API_URL, THINK_API_URL } from '@/config/setting';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 分页查询角色
|
* 分页查询角色
|
||||||
@@ -117,3 +117,45 @@ export async function updateRoleMenus(roleId?: number, data?: number[]) {
|
|||||||
}
|
}
|
||||||
return Promise.reject(new Error(res.data.message));
|
return Promise.reject(new Error(res.data.message));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分页查询角色成员
|
||||||
|
*/
|
||||||
|
export async function pageRoleMembers(params: RoleMemberParam) {
|
||||||
|
const res = await request.get<ApiResult<PageResult<RoleMember>>>(
|
||||||
|
THINK_API_URL + '/system/role-member/page',
|
||||||
|
{ params }
|
||||||
|
);
|
||||||
|
if (res.data.code === 0) {
|
||||||
|
return res.data.data;
|
||||||
|
}
|
||||||
|
return Promise.reject(new Error(res.data.message));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加角色成员
|
||||||
|
*/
|
||||||
|
export async function addRoleMember(data: RoleMemberParam) {
|
||||||
|
const res = await request.post<ApiResult<unknown>>(
|
||||||
|
THINK_API_URL + '/system/role-member',
|
||||||
|
data
|
||||||
|
);
|
||||||
|
if (res.data.code === 0) {
|
||||||
|
return res.data.message;
|
||||||
|
}
|
||||||
|
return Promise.reject(new Error(res.data.message));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 移除角色成员
|
||||||
|
*/
|
||||||
|
export async function removeRoleMember(roleId?: number, userId?: number) {
|
||||||
|
const res = await request.delete<ApiResult<unknown>>(
|
||||||
|
THINK_API_URL + '/system/role-member',
|
||||||
|
{ params: { roleId, userId } }
|
||||||
|
);
|
||||||
|
if (res.data.code === 0) {
|
||||||
|
return res.data.message;
|
||||||
|
}
|
||||||
|
return Promise.reject(new Error(res.data.message));
|
||||||
|
}
|
||||||
|
|||||||
@@ -25,3 +25,31 @@ export interface RoleParam extends PageParam {
|
|||||||
roleCode?: string;
|
roleCode?: string;
|
||||||
comments?: string;
|
comments?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 角色成员
|
||||||
|
*/
|
||||||
|
export interface RoleMember {
|
||||||
|
// 用户id
|
||||||
|
userId?: number;
|
||||||
|
// 账号
|
||||||
|
username?: string;
|
||||||
|
// 真实姓名
|
||||||
|
realName?: string;
|
||||||
|
// 昵称
|
||||||
|
nickname?: string;
|
||||||
|
// 所属组织id
|
||||||
|
organizationId?: number;
|
||||||
|
// 所属组织名称
|
||||||
|
organizationName?: string;
|
||||||
|
// 状态, 0启用, 1停用
|
||||||
|
status?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 角色成员查询/操作参数
|
||||||
|
*/
|
||||||
|
export interface RoleMemberParam extends PageParam {
|
||||||
|
roleId?: number;
|
||||||
|
userId?: number;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import request from '@/utils/request';
|
import request from '@/utils/request';
|
||||||
import type { ApiResult, PageResult } from '@/api';
|
import type { ApiResult, PageResult } from '@/api';
|
||||||
import type { User, UserParam } from './model';
|
import type { User, UserParam } from './model';
|
||||||
import { SERVER_API_URL } from '@/config/setting';
|
import { SERVER_API_URL, THINK_API_URL } from '@/config/setting';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 分页查询用户
|
* 分页查询用户
|
||||||
@@ -97,7 +97,7 @@ export async function addUser(data: User) {
|
|||||||
*/
|
*/
|
||||||
export async function updateUser(data: User) {
|
export async function updateUser(data: User) {
|
||||||
const res = await request.put<ApiResult<unknown>>(
|
const res = await request.put<ApiResult<unknown>>(
|
||||||
SERVER_API_URL + '/system/user',
|
THINK_API_URL + '/system/user',
|
||||||
data
|
data
|
||||||
);
|
);
|
||||||
if (res.data.code === 0) {
|
if (res.data.code === 0) {
|
||||||
|
|||||||
@@ -0,0 +1,280 @@
|
|||||||
|
<!-- 单据详情抽屉(只读) -->
|
||||||
|
<template>
|
||||||
|
<ele-modal
|
||||||
|
title="单据详情"
|
||||||
|
:width="860"
|
||||||
|
:footer="null"
|
||||||
|
:visible="visible"
|
||||||
|
@update:visible="updateVisible"
|
||||||
|
>
|
||||||
|
<a-spin :spinning="loading">
|
||||||
|
<template v-if="data">
|
||||||
|
<a-descriptions
|
||||||
|
:column="2"
|
||||||
|
size="small"
|
||||||
|
bordered
|
||||||
|
:label-style="{ width: '110px' }"
|
||||||
|
>
|
||||||
|
<a-descriptions-item label="单号">
|
||||||
|
{{ data.bizNo || '-' }}
|
||||||
|
</a-descriptions-item>
|
||||||
|
<a-descriptions-item label="单据类型">
|
||||||
|
{{ data.docTypeName || '-' }}
|
||||||
|
</a-descriptions-item>
|
||||||
|
<a-descriptions-item label="状态">
|
||||||
|
<a-tag :color="statusColor(data.status)">
|
||||||
|
{{ data.statusName || '-' }}
|
||||||
|
</a-tag>
|
||||||
|
</a-descriptions-item>
|
||||||
|
<a-descriptions-item label="当前节点">
|
||||||
|
{{ data.currentNodeName || '-' }}
|
||||||
|
</a-descriptions-item>
|
||||||
|
<a-descriptions-item label="报送人">
|
||||||
|
{{ data.reporterName || '-' }}
|
||||||
|
</a-descriptions-item>
|
||||||
|
<a-descriptions-item label="报送部门">
|
||||||
|
{{ data.reportOrgName || '-' }}
|
||||||
|
</a-descriptions-item>
|
||||||
|
<a-descriptions-item label="受理部门">
|
||||||
|
{{ data.acceptOrgName || '-' }}
|
||||||
|
</a-descriptions-item>
|
||||||
|
<a-descriptions-item label="提交时间">
|
||||||
|
{{ toDateString(data.submitTime) || '-' }}
|
||||||
|
</a-descriptions-item>
|
||||||
|
<template v-if="data.docType === 'completion'">
|
||||||
|
<a-descriptions-item label="名称" :span="2">
|
||||||
|
{{ data.name || '-' }}
|
||||||
|
</a-descriptions-item>
|
||||||
|
<a-descriptions-item label="实施单位">
|
||||||
|
{{ data.implementOrg || '-' }}
|
||||||
|
</a-descriptions-item>
|
||||||
|
<a-descriptions-item label="费用(元)">
|
||||||
|
{{ data.fee ?? '-' }}
|
||||||
|
</a-descriptions-item>
|
||||||
|
<a-descriptions-item label="服务内容" :span="2">
|
||||||
|
<div class="ele-text-break">{{ data.serviceContent || '-' }}</div>
|
||||||
|
</a-descriptions-item>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<a-descriptions-item label="标题" :span="2">
|
||||||
|
{{ data.title || '-' }}
|
||||||
|
</a-descriptions-item>
|
||||||
|
<a-descriptions-item label="协调内容" :span="2">
|
||||||
|
<div class="ele-text-break">{{ data.content || '-' }}</div>
|
||||||
|
</a-descriptions-item>
|
||||||
|
<a-descriptions-item label="备注" :span="2">
|
||||||
|
<div class="ele-text-break">{{ data.remark || '-' }}</div>
|
||||||
|
</a-descriptions-item>
|
||||||
|
<a-descriptions-item label="派单对象">
|
||||||
|
{{ data.dispatchTargetName || '-' }}
|
||||||
|
</a-descriptions-item>
|
||||||
|
<a-descriptions-item label="审批完成时间">
|
||||||
|
{{ toDateString(data.approveTime) || '-' }}
|
||||||
|
</a-descriptions-item>
|
||||||
|
</template>
|
||||||
|
<a-descriptions-item label="归档时间">
|
||||||
|
{{ toDateString(data.archiveTime) || '-' }}
|
||||||
|
</a-descriptions-item>
|
||||||
|
<a-descriptions-item label="归档人">
|
||||||
|
{{ data.archivistName || '-' }}
|
||||||
|
</a-descriptions-item>
|
||||||
|
</a-descriptions>
|
||||||
|
|
||||||
|
<a-divider orientation="left">附件</a-divider>
|
||||||
|
<div v-if="data.attachments && data.attachments.length">
|
||||||
|
<div
|
||||||
|
v-for="attachment in data.attachments"
|
||||||
|
:key="attachment.id"
|
||||||
|
class="attachment-item"
|
||||||
|
>
|
||||||
|
<a :href="attachment.url" target="_blank" rel="noopener noreferrer">
|
||||||
|
{{ attachment.name || '附件' }}
|
||||||
|
</a>
|
||||||
|
<a-tag style="margin-left: 8px">
|
||||||
|
{{ attachment.categoryName || '-' }}
|
||||||
|
</a-tag>
|
||||||
|
<span class="ele-text-secondary" style="margin-left: 8px">
|
||||||
|
{{ sizeText(attachment.length) }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-else class="ele-text-secondary">暂无附件</div>
|
||||||
|
|
||||||
|
<a-divider orientation="left">存档确认</a-divider>
|
||||||
|
<div v-if="data.confirms && data.confirms.length">
|
||||||
|
<div
|
||||||
|
v-for="confirm in data.confirms"
|
||||||
|
:key="confirm.userId"
|
||||||
|
class="confirm-item"
|
||||||
|
>
|
||||||
|
<span>{{ confirm.userName || '-' }}</span>
|
||||||
|
<a-tag
|
||||||
|
style="margin-left: 8px"
|
||||||
|
:color="confirm.status === 'DONE' ? 'green' : 'default'"
|
||||||
|
>
|
||||||
|
{{ confirm.statusName || '-' }}
|
||||||
|
</a-tag>
|
||||||
|
<span class="ele-text-secondary" style="margin-left: 8px">
|
||||||
|
{{ toDateString(confirm.handleTime) || '' }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-else class="ele-text-secondary">尚未进入存档环节</div>
|
||||||
|
|
||||||
|
<a-divider orientation="left">处理记录</a-divider>
|
||||||
|
<a-timeline v-if="data.records && data.records.length">
|
||||||
|
<a-timeline-item
|
||||||
|
v-for="record in data.records"
|
||||||
|
:key="record.id"
|
||||||
|
:color="actionColor(record.action)"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<span style="font-weight: 500">
|
||||||
|
{{ record.operatorName || '-' }}
|
||||||
|
</span>
|
||||||
|
<span style="margin-left: 8px">
|
||||||
|
{{ record.actionName || record.action }}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
v-if="record.nodeName"
|
||||||
|
class="ele-text-secondary"
|
||||||
|
style="margin-left: 8px"
|
||||||
|
>
|
||||||
|
({{ record.nodeName }})
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div v-if="record.opinion" class="ele-text-secondary">
|
||||||
|
意见:{{ record.opinion }}
|
||||||
|
</div>
|
||||||
|
<div class="ele-text-secondary">
|
||||||
|
{{ toDateString(record.createTime) }}
|
||||||
|
</div>
|
||||||
|
</a-timeline-item>
|
||||||
|
</a-timeline>
|
||||||
|
<div v-else class="ele-text-secondary">暂无处理记录</div>
|
||||||
|
</template>
|
||||||
|
<a-empty v-else-if="!loading" description="暂无数据" />
|
||||||
|
</a-spin>
|
||||||
|
</ele-modal>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { ref, watch } from 'vue';
|
||||||
|
import { message } from 'ant-design-vue/es';
|
||||||
|
import { toDateString } from 'ele-admin-pro/es';
|
||||||
|
import { getInstance } from '@/api/approval/instance';
|
||||||
|
import type { ApprovalInstance } from '@/api/approval/instance/model';
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'update:visible', visible: boolean): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
// 抽屉是否打开
|
||||||
|
visible: boolean;
|
||||||
|
// 单据id
|
||||||
|
instanceId?: number;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
// 详情数据
|
||||||
|
const data = ref<ApprovalInstance | null>(null);
|
||||||
|
// 加载状态
|
||||||
|
const loading = ref(false);
|
||||||
|
|
||||||
|
// 状态颜色
|
||||||
|
const statusColor = (status?: string) => {
|
||||||
|
switch (status) {
|
||||||
|
case 'PENDING':
|
||||||
|
return 'processing';
|
||||||
|
case 'REJECTED':
|
||||||
|
return 'red';
|
||||||
|
case 'WITHDRAWN':
|
||||||
|
return 'orange';
|
||||||
|
case 'APPROVED':
|
||||||
|
return 'green';
|
||||||
|
case 'ARCHIVED':
|
||||||
|
return 'blue';
|
||||||
|
default:
|
||||||
|
return 'default';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 处理记录颜色
|
||||||
|
const actionColor = (action?: string) => {
|
||||||
|
switch (action) {
|
||||||
|
case 'APPROVE':
|
||||||
|
case 'CONFIRM':
|
||||||
|
case 'ARCHIVE':
|
||||||
|
return 'green';
|
||||||
|
case 'REJECT':
|
||||||
|
return 'red';
|
||||||
|
default:
|
||||||
|
return 'blue';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 附件大小
|
||||||
|
const sizeText = (length?: number) => {
|
||||||
|
if (!length && length !== 0) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
if (length < 1024) {
|
||||||
|
return length + ' B';
|
||||||
|
}
|
||||||
|
if (length < 1024 * 1024) {
|
||||||
|
return (length / 1024).toFixed(1) + ' KB';
|
||||||
|
}
|
||||||
|
return (length / 1024 / 1024).toFixed(1) + ' MB';
|
||||||
|
};
|
||||||
|
|
||||||
|
/* 更新visible */
|
||||||
|
const updateVisible = (value: boolean) => {
|
||||||
|
emit('update:visible', value);
|
||||||
|
};
|
||||||
|
|
||||||
|
/* 加载详情 */
|
||||||
|
const loadData = () => {
|
||||||
|
if (!props.instanceId) {
|
||||||
|
data.value = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
loading.value = true;
|
||||||
|
data.value = null;
|
||||||
|
getInstance(props.instanceId)
|
||||||
|
.then((result) => {
|
||||||
|
data.value = result;
|
||||||
|
loading.value = false;
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
loading.value = false;
|
||||||
|
message.error(e.message);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => [props.visible, props.instanceId],
|
||||||
|
() => {
|
||||||
|
if (props.visible) {
|
||||||
|
loadData();
|
||||||
|
} else {
|
||||||
|
data.value = null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.attachment-item {
|
||||||
|
padding: 4px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.confirm-item {
|
||||||
|
padding: 4px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ele-text-break {
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,254 @@
|
|||||||
|
<!-- 单据查询搜索表单 -->
|
||||||
|
<template>
|
||||||
|
<a-form
|
||||||
|
:label-col="
|
||||||
|
styleResponsive ? { xl: 7, lg: 5, md: 7, sm: 4 } : { flex: '90px' }
|
||||||
|
"
|
||||||
|
:wrapper-col="
|
||||||
|
styleResponsive ? { xl: 17, lg: 19, md: 17, sm: 20 } : { flex: '1' }
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<a-row :gutter="8">
|
||||||
|
<a-col v-for="field in basicFields" :key="field.prop" v-bind="colSpan">
|
||||||
|
<a-form-item :label="field.label">
|
||||||
|
<a-select
|
||||||
|
v-if="field.prop === 'docType'"
|
||||||
|
v-model:value="form.docType"
|
||||||
|
placeholder="全部"
|
||||||
|
allow-clear
|
||||||
|
:options="docTypeOptions"
|
||||||
|
/>
|
||||||
|
<a-select
|
||||||
|
v-else-if="field.prop === 'status'"
|
||||||
|
v-model:value="form.status"
|
||||||
|
placeholder="全部"
|
||||||
|
allow-clear
|
||||||
|
:options="statusOptions"
|
||||||
|
/>
|
||||||
|
<a-select
|
||||||
|
v-else-if="field.prop === 'reportOrgId'"
|
||||||
|
v-model:value="form.reportOrgId"
|
||||||
|
placeholder="全部"
|
||||||
|
allow-clear
|
||||||
|
show-search
|
||||||
|
:filter-option="filterOrganization"
|
||||||
|
:options="organizationOptions"
|
||||||
|
/>
|
||||||
|
<a-select
|
||||||
|
v-else-if="field.prop === 'acceptOrgId'"
|
||||||
|
v-model:value="form.acceptOrgId"
|
||||||
|
placeholder="全部"
|
||||||
|
allow-clear
|
||||||
|
show-search
|
||||||
|
:filter-option="filterOrganization"
|
||||||
|
:options="organizationOptions"
|
||||||
|
/>
|
||||||
|
<a-select
|
||||||
|
v-else-if="field.prop === 'reporterId'"
|
||||||
|
v-model:value="form.reporterId"
|
||||||
|
placeholder="输入姓名或账号搜索"
|
||||||
|
allow-clear
|
||||||
|
show-search
|
||||||
|
:filter-option="false"
|
||||||
|
:options="userOptions"
|
||||||
|
:loading="userLoading"
|
||||||
|
@search="searchUsers"
|
||||||
|
/>
|
||||||
|
<a-input
|
||||||
|
v-else
|
||||||
|
v-model:value.trim="form[field.prop]"
|
||||||
|
placeholder="请输入"
|
||||||
|
allow-clear
|
||||||
|
/>
|
||||||
|
</a-form-item>
|
||||||
|
</a-col>
|
||||||
|
<a-col v-bind="colSpan">
|
||||||
|
<a-form-item label="提交时间">
|
||||||
|
<a-range-picker
|
||||||
|
v-model:value="submitRange"
|
||||||
|
:show-time="true"
|
||||||
|
value-format="YYYY-MM-DD HH:mm:ss"
|
||||||
|
class="ele-fluid"
|
||||||
|
/>
|
||||||
|
</a-form-item>
|
||||||
|
</a-col>
|
||||||
|
<a-col v-bind="colSpan">
|
||||||
|
<a-form-item label="归档时间">
|
||||||
|
<a-range-picker
|
||||||
|
v-model:value="archiveRange"
|
||||||
|
:show-time="true"
|
||||||
|
value-format="YYYY-MM-DD HH:mm:ss"
|
||||||
|
class="ele-fluid"
|
||||||
|
/>
|
||||||
|
</a-form-item>
|
||||||
|
</a-col>
|
||||||
|
<a-col v-bind="colSpan">
|
||||||
|
<a-form-item class="ele-text-right" :wrapper-col="{ span: 24 }">
|
||||||
|
<a-space>
|
||||||
|
<a-button type="primary" @click="search">查询</a-button>
|
||||||
|
<a-button @click="reset">重置</a-button>
|
||||||
|
</a-space>
|
||||||
|
</a-form-item>
|
||||||
|
</a-col>
|
||||||
|
</a-row>
|
||||||
|
</a-form>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { onMounted, ref } from 'vue';
|
||||||
|
import { storeToRefs } from 'pinia';
|
||||||
|
import { useThemeStore } from '@/store/modules/theme';
|
||||||
|
import useFormData from '@/utils/use-form-data';
|
||||||
|
import {
|
||||||
|
listApprovalOrganizations,
|
||||||
|
searchApprovalUsers
|
||||||
|
} from '@/api/approval/directory';
|
||||||
|
import type {
|
||||||
|
ApprovalOrganization,
|
||||||
|
ApprovalQueryParam,
|
||||||
|
ApprovalUser
|
||||||
|
} from '@/api/approval/instance/model';
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'search', where?: ApprovalQueryParam): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
// 是否开启响应式布局
|
||||||
|
const themeStore = useThemeStore();
|
||||||
|
const { styleResponsive } = storeToRefs(themeStore);
|
||||||
|
|
||||||
|
// 栅格宽度
|
||||||
|
const colSpan = {
|
||||||
|
xl: 6,
|
||||||
|
lg: 12,
|
||||||
|
md: 12,
|
||||||
|
sm: 24,
|
||||||
|
xs: 24
|
||||||
|
};
|
||||||
|
|
||||||
|
// 基础字段
|
||||||
|
const basicFields: {
|
||||||
|
prop:
|
||||||
|
| 'docType'
|
||||||
|
| 'bizNo'
|
||||||
|
| 'keywords'
|
||||||
|
| 'reportOrgId'
|
||||||
|
| 'acceptOrgId'
|
||||||
|
| 'reporterId'
|
||||||
|
| 'status';
|
||||||
|
label: string;
|
||||||
|
}[] = [
|
||||||
|
{ prop: 'docType', label: '单据类型' },
|
||||||
|
{ prop: 'bizNo', label: '单号' },
|
||||||
|
{ prop: 'keywords', label: '标题或名称' },
|
||||||
|
{ prop: 'reportOrgId', label: '报送部门' },
|
||||||
|
{ prop: 'acceptOrgId', label: '受理部门' },
|
||||||
|
{ prop: 'reporterId', label: '报送人' },
|
||||||
|
{ prop: 'status', label: '状态' }
|
||||||
|
];
|
||||||
|
|
||||||
|
// 单据类型选项
|
||||||
|
const docTypeOptions = [
|
||||||
|
{ label: '业务完成确认单', value: 'completion' },
|
||||||
|
{ label: '内部协调单', value: 'coordination' }
|
||||||
|
];
|
||||||
|
|
||||||
|
// 状态选项
|
||||||
|
const statusOptions = [
|
||||||
|
{ label: '未提交', value: 'DRAFT' },
|
||||||
|
{ label: '审批中', value: 'PENDING' },
|
||||||
|
{ label: '已驳回', value: 'REJECTED' },
|
||||||
|
{ label: '已撤回', value: 'WITHDRAWN' },
|
||||||
|
{ label: '审批完成', value: 'APPROVED' },
|
||||||
|
{ label: '已归档', value: 'ARCHIVED' }
|
||||||
|
];
|
||||||
|
|
||||||
|
// 表单数据
|
||||||
|
const { form, resetFields } = useFormData<ApprovalQueryParam>({
|
||||||
|
docType: undefined,
|
||||||
|
bizNo: '',
|
||||||
|
keywords: '',
|
||||||
|
reportOrgId: undefined,
|
||||||
|
acceptOrgId: undefined,
|
||||||
|
reporterId: undefined,
|
||||||
|
status: undefined
|
||||||
|
});
|
||||||
|
|
||||||
|
// 提交时间范围
|
||||||
|
const submitRange = ref<[string, string]>(['', '']);
|
||||||
|
// 归档时间范围
|
||||||
|
const archiveRange = ref<[string, string]>(['', '']);
|
||||||
|
|
||||||
|
// 组织下拉选项
|
||||||
|
const organizationOptions = ref<{ label: string; value: number }[]>([]);
|
||||||
|
|
||||||
|
// 报送人下拉选项
|
||||||
|
const userOptions = ref<{ label: string; value: number }[]>([]);
|
||||||
|
// 报送人请求状态
|
||||||
|
const userLoading = ref(false);
|
||||||
|
|
||||||
|
// 组织远程过滤
|
||||||
|
const filterOrganization = (input: string, option: { label: string }) => {
|
||||||
|
return (option?.label ?? '').toLowerCase().includes(input.toLowerCase());
|
||||||
|
};
|
||||||
|
|
||||||
|
// 远程搜索报送人
|
||||||
|
const searchUsers = (keywords?: string) => {
|
||||||
|
userLoading.value = true;
|
||||||
|
searchApprovalUsers({ keywords })
|
||||||
|
.then((data) => {
|
||||||
|
userOptions.value = data.map((user: ApprovalUser) => ({
|
||||||
|
label:
|
||||||
|
(user.realName || user.nickname || user.username || '') +
|
||||||
|
(user.username ? '(' + user.username + ')' : ''),
|
||||||
|
value: user.userId as number
|
||||||
|
}));
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
userOptions.value = [];
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
userLoading.value = false;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/* 搜索 */
|
||||||
|
const search = () => {
|
||||||
|
const [submitTimeStart, submitTimeEnd] = submitRange.value ?? ['', ''];
|
||||||
|
const [archiveTimeStart, archiveTimeEnd] = archiveRange.value ?? ['', ''];
|
||||||
|
emit('search', {
|
||||||
|
...form,
|
||||||
|
submitTimeStart,
|
||||||
|
submitTimeEnd,
|
||||||
|
archiveTimeStart,
|
||||||
|
archiveTimeEnd
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/* 重置 */
|
||||||
|
const reset = () => {
|
||||||
|
resetFields();
|
||||||
|
submitRange.value = ['', ''];
|
||||||
|
archiveRange.value = ['', ''];
|
||||||
|
userOptions.value = [];
|
||||||
|
search();
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
listApprovalOrganizations()
|
||||||
|
.then((data) => {
|
||||||
|
organizationOptions.value = data.map(
|
||||||
|
(organization: ApprovalOrganization) => ({
|
||||||
|
label:
|
||||||
|
organization.organizationFullName ||
|
||||||
|
organization.organizationName ||
|
||||||
|
'',
|
||||||
|
value: organization.organizationId as number
|
||||||
|
})
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
organizationOptions.value = [];
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,231 @@
|
|||||||
|
<!-- 审批管理 - 单据查询 -->
|
||||||
|
<template>
|
||||||
|
<div class="ele-body">
|
||||||
|
<a-card :bordered="false">
|
||||||
|
<!-- 搜索表单 -->
|
||||||
|
<search @search="reload" />
|
||||||
|
<!-- 表格 -->
|
||||||
|
<ele-pro-table
|
||||||
|
ref="tableRef"
|
||||||
|
row-key="id"
|
||||||
|
:columns="columns"
|
||||||
|
:datasource="datasource"
|
||||||
|
:scroll="{ x: 1500 }"
|
||||||
|
cache-key="proApprovalInstanceTable"
|
||||||
|
>
|
||||||
|
<template #toolbar>
|
||||||
|
<a-space>
|
||||||
|
<a-button
|
||||||
|
v-permission="'approval:instance:export'"
|
||||||
|
type="primary"
|
||||||
|
class="ele-btn-icon"
|
||||||
|
:loading="exporting"
|
||||||
|
@click="handleExport"
|
||||||
|
>
|
||||||
|
<template #icon>
|
||||||
|
<download-outlined />
|
||||||
|
</template>
|
||||||
|
<span>导出</span>
|
||||||
|
</a-button>
|
||||||
|
</a-space>
|
||||||
|
</template>
|
||||||
|
<template #bodyCell="{ column, record }">
|
||||||
|
<template v-if="column.key === 'docType'">
|
||||||
|
<a-tag :color="record.docType === 'completion' ? 'blue' : 'purple'">
|
||||||
|
{{ record.docTypeName }}
|
||||||
|
</a-tag>
|
||||||
|
</template>
|
||||||
|
<template v-else-if="column.key === 'status'">
|
||||||
|
<a-tag :color="statusColor(record.status)">
|
||||||
|
{{ record.statusName }}
|
||||||
|
</a-tag>
|
||||||
|
</template>
|
||||||
|
<template v-else-if="column.key === 'submitTime'">
|
||||||
|
{{ toDateString(record.submitTime) || '-' }}
|
||||||
|
</template>
|
||||||
|
<template v-else-if="column.key === 'archiveTime'">
|
||||||
|
{{ toDateString(record.archiveTime) || '-' }}
|
||||||
|
</template>
|
||||||
|
<template v-else-if="column.key === 'action'">
|
||||||
|
<a @click="openDetail(record)">详情</a>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
</ele-pro-table>
|
||||||
|
</a-card>
|
||||||
|
<!-- 详情抽屉 -->
|
||||||
|
<detail v-model:visible="showDetail" :instance-id="currentId" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { ref } from 'vue';
|
||||||
|
import { DownloadOutlined } from '@ant-design/icons-vue';
|
||||||
|
import { message } from 'ant-design-vue';
|
||||||
|
import type { EleProTable } from 'ele-admin-pro/es';
|
||||||
|
import { toDateString } from 'ele-admin-pro/es';
|
||||||
|
import type {
|
||||||
|
DatasourceFunction,
|
||||||
|
ColumnItem
|
||||||
|
} from 'ele-admin-pro/es/ele-pro-table/types';
|
||||||
|
import Search from './components/search.vue';
|
||||||
|
import Detail from './components/detail.vue';
|
||||||
|
import { exportInstances, pageInstances } from '@/api/approval/instance';
|
||||||
|
import type {
|
||||||
|
ApprovalItem,
|
||||||
|
ApprovalQueryParam
|
||||||
|
} from '@/api/approval/instance/model';
|
||||||
|
|
||||||
|
// 表格实例
|
||||||
|
const tableRef = ref<InstanceType<typeof EleProTable> | null>(null);
|
||||||
|
|
||||||
|
// 详情抽屉是否显示
|
||||||
|
const showDetail = ref(false);
|
||||||
|
// 当前查看的单据id
|
||||||
|
const currentId = ref<number | undefined>(undefined);
|
||||||
|
|
||||||
|
// 当前筛选条件(导出复用同一套条件)
|
||||||
|
const queryWhere = ref<ApprovalQueryParam>({});
|
||||||
|
// 导出请求状态
|
||||||
|
const exporting = ref(false);
|
||||||
|
|
||||||
|
// 表格数据源
|
||||||
|
const datasource: DatasourceFunction = ({ page, limit, where }) => {
|
||||||
|
return pageInstances({
|
||||||
|
...(where as ApprovalQueryParam),
|
||||||
|
page,
|
||||||
|
limit
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 表格列配置
|
||||||
|
const columns = ref<ColumnItem[]>([
|
||||||
|
{
|
||||||
|
title: '单号',
|
||||||
|
dataIndex: 'bizNo',
|
||||||
|
width: 180,
|
||||||
|
ellipsis: true,
|
||||||
|
customRender: ({ text }) => text || '-'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '单据类型',
|
||||||
|
key: 'docType',
|
||||||
|
dataIndex: 'docType',
|
||||||
|
width: 140,
|
||||||
|
align: 'center'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '标题或名称',
|
||||||
|
dataIndex: 'title',
|
||||||
|
minWidth: 220,
|
||||||
|
ellipsis: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '报送部门',
|
||||||
|
dataIndex: 'reportOrgName',
|
||||||
|
width: 180,
|
||||||
|
ellipsis: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '报送人',
|
||||||
|
dataIndex: 'reporterName',
|
||||||
|
width: 120,
|
||||||
|
ellipsis: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '受理部门',
|
||||||
|
dataIndex: 'acceptOrgName',
|
||||||
|
width: 180,
|
||||||
|
ellipsis: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '状态',
|
||||||
|
key: 'status',
|
||||||
|
dataIndex: 'status',
|
||||||
|
width: 110,
|
||||||
|
align: 'center'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '当前节点',
|
||||||
|
dataIndex: 'currentNodeName',
|
||||||
|
width: 140,
|
||||||
|
ellipsis: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '提交时间',
|
||||||
|
key: 'submitTime',
|
||||||
|
dataIndex: 'submitTime',
|
||||||
|
width: 170,
|
||||||
|
align: 'center'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '归档时间',
|
||||||
|
key: 'archiveTime',
|
||||||
|
dataIndex: 'archiveTime',
|
||||||
|
width: 170,
|
||||||
|
align: 'center'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '操作',
|
||||||
|
key: 'action',
|
||||||
|
width: 90,
|
||||||
|
align: 'center',
|
||||||
|
fixed: 'right',
|
||||||
|
hideInSetting: true
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
|
// 状态颜色
|
||||||
|
const statusColor = (status?: string) => {
|
||||||
|
switch (status) {
|
||||||
|
case 'PENDING':
|
||||||
|
return 'processing';
|
||||||
|
case 'REJECTED':
|
||||||
|
return 'red';
|
||||||
|
case 'WITHDRAWN':
|
||||||
|
return 'orange';
|
||||||
|
case 'APPROVED':
|
||||||
|
return 'green';
|
||||||
|
case 'ARCHIVED':
|
||||||
|
return 'blue';
|
||||||
|
default:
|
||||||
|
return 'default';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/* 刷新表格 */
|
||||||
|
const reload = (where?: ApprovalQueryParam) => {
|
||||||
|
queryWhere.value = where ?? {};
|
||||||
|
tableRef?.value?.reload({ page: 1, where: queryWhere.value });
|
||||||
|
};
|
||||||
|
|
||||||
|
/* 导出当前筛选结果 */
|
||||||
|
const handleExport = () => {
|
||||||
|
exporting.value = true;
|
||||||
|
exportInstances(queryWhere.value)
|
||||||
|
.then((res) => {
|
||||||
|
if (res.data?.url) {
|
||||||
|
window.open(res.data.url);
|
||||||
|
} else {
|
||||||
|
message.error('导出失败:未返回文件地址');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((e: Error) => {
|
||||||
|
message.error(e.message || '导出失败');
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
exporting.value = false;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/* 打开详情 */
|
||||||
|
const openDetail = (row: ApprovalItem) => {
|
||||||
|
currentId.value = row.id;
|
||||||
|
showDetail.value = true;
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
export default {
|
||||||
|
name: 'ApprovalInstance'
|
||||||
|
};
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,695 @@
|
|||||||
|
<!-- 审批待办详情抽屉 -->
|
||||||
|
<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?.canDispatch"
|
||||||
|
v-permission="'approval:instance:approve'"
|
||||||
|
type="primary"
|
||||||
|
@click="openDispatch"
|
||||||
|
>
|
||||||
|
派单
|
||||||
|
</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="pendingTasks.length">
|
||||||
|
<a-tag v-for="task in pendingTasks" :key="task.id" color="blue">
|
||||||
|
{{ task.nodeName }}·{{ task.assigneeName }}
|
||||||
|
</a-tag>
|
||||||
|
</template>
|
||||||
|
<span v-else>-</span>
|
||||||
|
</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="dispatchVisible"
|
||||||
|
title="派单"
|
||||||
|
:confirm-loading="submitting"
|
||||||
|
:mask-closable="false"
|
||||||
|
@ok="submitDispatch"
|
||||||
|
>
|
||||||
|
<a-form layout="vertical">
|
||||||
|
<a-form-item label="派单对象" required>
|
||||||
|
<a-select
|
||||||
|
v-model:value="dispatchTargetUserId"
|
||||||
|
show-search
|
||||||
|
:filter-option="filterCandidate"
|
||||||
|
:options="candidateOptions"
|
||||||
|
:loading="candidateLoading"
|
||||||
|
placeholder="请选择受理部门成员"
|
||||||
|
not-found-content="暂无可派单人员,请联系管理员维护部门成员"
|
||||||
|
/>
|
||||||
|
</a-form-item>
|
||||||
|
<div class="ele-text-secondary">
|
||||||
|
派单后由报送人与被派单人分别存档,选定后不可更改。
|
||||||
|
</div>
|
||||||
|
</a-form>
|
||||||
|
</a-modal>
|
||||||
|
|
||||||
|
<!-- 改派 -->
|
||||||
|
<a-modal
|
||||||
|
v-model:visible="assignVisible"
|
||||||
|
title="改派待办"
|
||||||
|
:confirm-loading="submitting"
|
||||||
|
:mask-closable="false"
|
||||||
|
@ok="submitAssign"
|
||||||
|
>
|
||||||
|
<a-form layout="vertical">
|
||||||
|
<a-form-item v-if="pendingTasks.length" label="待办任务" required>
|
||||||
|
<a-select
|
||||||
|
v-model:value="assignTaskId"
|
||||||
|
:options="pendingTaskOptions"
|
||||||
|
placeholder="请选择需要改派的待办任务"
|
||||||
|
/>
|
||||||
|
</a-form-item>
|
||||||
|
<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,
|
||||||
|
ApprovalPendingTask,
|
||||||
|
ApprovalUser
|
||||||
|
} from '@/api/approval/instance/model';
|
||||||
|
import { getInstance } from '@/api/approval/instance';
|
||||||
|
import { searchApprovalUsers } from '@/api/approval/directory';
|
||||||
|
import {
|
||||||
|
approveApprovalTask,
|
||||||
|
archiveApprovalTask,
|
||||||
|
assignApprovalTask,
|
||||||
|
confirmApprovalTask,
|
||||||
|
dispatchApprovalTask,
|
||||||
|
listDispatchCandidates,
|
||||||
|
rejectApprovalTask
|
||||||
|
} 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 pendingTasks = computed<ApprovalPendingTask[]>(
|
||||||
|
() => detail.value?.pendingTasks ?? []
|
||||||
|
);
|
||||||
|
|
||||||
|
const load = async () => {
|
||||||
|
if (!props.instanceId) {
|
||||||
|
detail.value = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
loading.value = true;
|
||||||
|
try {
|
||||||
|
detail.value = await getInstance(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 dispatchVisible = ref(false);
|
||||||
|
const candidateLoading = ref(false);
|
||||||
|
const dispatchTargetUserId = ref<number | undefined>(undefined);
|
||||||
|
const candidateOptions = ref<{ label: string; value: number }[]>([]);
|
||||||
|
|
||||||
|
const candidateLabel = (user: ApprovalUser) =>
|
||||||
|
(user.realName || user.nickname || user.username || '') +
|
||||||
|
(user.username ? '(' + user.username + ')' : '') +
|
||||||
|
(user.organizationName ? ' · ' + user.organizationName : '');
|
||||||
|
|
||||||
|
const filterCandidate = (input: string, option: any) =>
|
||||||
|
String(option?.label ?? '')
|
||||||
|
.toLowerCase()
|
||||||
|
.includes(input.toLowerCase());
|
||||||
|
|
||||||
|
const openDispatch = async () => {
|
||||||
|
const taskId = detail.value?.myTask?.id;
|
||||||
|
if (!taskId) {
|
||||||
|
message.error('待办任务不存在,请刷新');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
dispatchTargetUserId.value = undefined;
|
||||||
|
candidateOptions.value = [];
|
||||||
|
dispatchVisible.value = true;
|
||||||
|
candidateLoading.value = true;
|
||||||
|
try {
|
||||||
|
const users = await listDispatchCandidates(taskId);
|
||||||
|
candidateOptions.value = users.map((user) => ({
|
||||||
|
value: user.userId as number,
|
||||||
|
label: candidateLabel(user)
|
||||||
|
}));
|
||||||
|
if (!candidateOptions.value.length) {
|
||||||
|
message.warning('暂无可派单人员,请联系管理员维护部门成员');
|
||||||
|
}
|
||||||
|
} catch (e: any) {
|
||||||
|
message.error(e.message);
|
||||||
|
} finally {
|
||||||
|
candidateLoading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const submitDispatch = async () => {
|
||||||
|
const taskId = detail.value?.myTask?.id;
|
||||||
|
if (!taskId) {
|
||||||
|
message.error('待办任务不存在,请刷新');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!dispatchTargetUserId.value) {
|
||||||
|
message.error('请选择派单对象');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
submitting.value = true;
|
||||||
|
try {
|
||||||
|
await dispatchApprovalTask(taskId, dispatchTargetUserId.value);
|
||||||
|
message.success('派单成功');
|
||||||
|
dispatchVisible.value = false;
|
||||||
|
await load();
|
||||||
|
emit('done');
|
||||||
|
} catch (e: any) {
|
||||||
|
message.error(e.message);
|
||||||
|
} finally {
|
||||||
|
submitting.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/* 改派 */
|
||||||
|
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: ''
|
||||||
|
});
|
||||||
|
const assignTaskId = ref<number | undefined>(undefined);
|
||||||
|
const pendingTaskOptions = computed(() =>
|
||||||
|
pendingTasks.value.map((task) => ({
|
||||||
|
value: task.id as number,
|
||||||
|
label: `${task.nodeName || task.nodeKey || ''}·${task.assigneeName || ''}`
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
let searchTimer: ReturnType<typeof setTimeout> | undefined;
|
||||||
|
|
||||||
|
const doSearchUsers = async (keywords?: string) => {
|
||||||
|
userLoading.value = true;
|
||||||
|
try {
|
||||||
|
const users = await searchApprovalUsers({
|
||||||
|
keywords: 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 = '';
|
||||||
|
assignTaskId.value = detail.value?.myTask?.id ?? pendingTasks.value[0]?.id;
|
||||||
|
assignVisible.value = true;
|
||||||
|
doSearchUsers();
|
||||||
|
};
|
||||||
|
|
||||||
|
const submitAssign = async () => {
|
||||||
|
const taskId = assignTaskId.value;
|
||||||
|
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');
|
||||||
|
if (detail.value?.myTask?.id === taskId) {
|
||||||
|
// 改派的是当前用户的待办,关闭抽屉回到列表
|
||||||
|
updateVisible(false);
|
||||||
|
} else {
|
||||||
|
await load();
|
||||||
|
}
|
||||||
|
} 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,231 @@
|
|||||||
|
<!-- 审批管理 - 待办审批 -->
|
||||||
|
<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-checkbox
|
||||||
|
v-if="canAssign"
|
||||||
|
v-model:checked="allScope"
|
||||||
|
@change="reload"
|
||||||
|
>
|
||||||
|
全部待办
|
||||||
|
</a-checkbox>
|
||||||
|
<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 { hasPermission } from '@/utils/permission';
|
||||||
|
import type {
|
||||||
|
ApprovalItem,
|
||||||
|
ApprovalTodoParam
|
||||||
|
} from '@/api/approval/instance/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);
|
||||||
|
// 是否查看全租户待办(仅 assign 权限)
|
||||||
|
const allScope = ref(false);
|
||||||
|
const canAssign = hasPermission('approval:instance:assign');
|
||||||
|
|
||||||
|
// 搜索条件
|
||||||
|
const { where, resetFields } = useSearch<ApprovalTodoParam>({
|
||||||
|
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,
|
||||||
|
scope: allScope.value ? 'all' : undefined,
|
||||||
|
page,
|
||||||
|
limit
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 重新加载表格
|
||||||
|
const reload = () => {
|
||||||
|
tableRef.value?.reload({ page: 1 });
|
||||||
|
};
|
||||||
|
|
||||||
|
// 重置搜索
|
||||||
|
const resetSearch = () => {
|
||||||
|
resetFields();
|
||||||
|
allScope.value = false;
|
||||||
|
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>
|
||||||
@@ -0,0 +1,230 @@
|
|||||||
|
<!-- 角色成员维护弹窗 -->
|
||||||
|
<template>
|
||||||
|
<ele-modal
|
||||||
|
:width="720"
|
||||||
|
title="角色成员"
|
||||||
|
:visible="visible"
|
||||||
|
:body-style="{ paddingBottom: '8px' }"
|
||||||
|
:footer="null"
|
||||||
|
@update:visible="updateVisible"
|
||||||
|
>
|
||||||
|
<div style="margin-bottom: 12px">
|
||||||
|
<a-space>
|
||||||
|
<a-select
|
||||||
|
v-model:value="selectedUserId"
|
||||||
|
show-search
|
||||||
|
allow-clear
|
||||||
|
:filter-option="false"
|
||||||
|
:options="userOptions"
|
||||||
|
:loading="userLoading"
|
||||||
|
placeholder="输入姓名或账号搜索启用用户"
|
||||||
|
style="width: 320px"
|
||||||
|
@search="searchUsers"
|
||||||
|
/>
|
||||||
|
<a-button
|
||||||
|
type="primary"
|
||||||
|
:loading="adding"
|
||||||
|
:disabled="!selectedUserId"
|
||||||
|
@click="addOne"
|
||||||
|
>
|
||||||
|
添加成员
|
||||||
|
</a-button>
|
||||||
|
</a-space>
|
||||||
|
<div class="ele-text-secondary" style="margin-top: 6px">
|
||||||
|
角色:{{ data?.roleName }}({{ data?.roleCode }})
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<a-table
|
||||||
|
size="small"
|
||||||
|
row-key="userId"
|
||||||
|
:columns="columns"
|
||||||
|
:data-source="members"
|
||||||
|
:loading="loading"
|
||||||
|
:pagination="false"
|
||||||
|
:scroll="{ y: '46vh' }"
|
||||||
|
>
|
||||||
|
<template #bodyCell="{ column, record }">
|
||||||
|
<template v-if="column.key === 'userName'">
|
||||||
|
{{ record.realName || record.nickname || '-' }}
|
||||||
|
</template>
|
||||||
|
<template v-else-if="column.key === 'organizationName'">
|
||||||
|
{{ record.organizationName || '-' }}
|
||||||
|
</template>
|
||||||
|
<template v-else-if="column.key === 'status'">
|
||||||
|
<a-tag :color="record.status === 0 ? 'green' : 'red'">
|
||||||
|
{{ record.status === 0 ? '启用' : '停用' }}
|
||||||
|
</a-tag>
|
||||||
|
</template>
|
||||||
|
<template v-else-if="column.key === 'action'">
|
||||||
|
<a-popconfirm
|
||||||
|
placement="topRight"
|
||||||
|
title="确定要移除该成员吗?"
|
||||||
|
@confirm="removeOne(record)"
|
||||||
|
>
|
||||||
|
<a class="ele-text-danger">移除</a>
|
||||||
|
</a-popconfirm>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
</a-table>
|
||||||
|
</ele-modal>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { computed, ref, watch } from 'vue';
|
||||||
|
import { message } from 'ant-design-vue/es';
|
||||||
|
import type { ColumnItem } from 'ele-admin-pro/es/ele-pro-table/types';
|
||||||
|
import {
|
||||||
|
addRoleMember,
|
||||||
|
pageRoleMembers,
|
||||||
|
removeRoleMember
|
||||||
|
} from '@/api/system/role';
|
||||||
|
import type { Role, RoleMember } from '@/api/system/role/model';
|
||||||
|
import { pageUsers } from '@/api/system/user';
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'update:visible', visible: boolean): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
// 弹窗是否打开
|
||||||
|
visible: boolean;
|
||||||
|
// 当前角色数据
|
||||||
|
data?: Role | null;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
// 表格列配置
|
||||||
|
const columns = ref<ColumnItem[]>([
|
||||||
|
{ title: '姓名', key: 'userName', width: 120 },
|
||||||
|
{ title: '账号', dataIndex: 'username', width: 150 },
|
||||||
|
{ title: '所属组织', key: 'organizationName' },
|
||||||
|
{ title: '状态', key: 'status', width: 90, align: 'center' },
|
||||||
|
{ title: '操作', key: 'action', width: 90, align: 'center' }
|
||||||
|
]);
|
||||||
|
|
||||||
|
// 成员列表
|
||||||
|
const members = ref<RoleMember[]>([]);
|
||||||
|
|
||||||
|
// 成员请求状态
|
||||||
|
const loading = ref(false);
|
||||||
|
|
||||||
|
// 添加状态
|
||||||
|
const adding = ref(false);
|
||||||
|
|
||||||
|
// 候选用户请求状态
|
||||||
|
const userLoading = ref(false);
|
||||||
|
|
||||||
|
// 候选用户下拉选项
|
||||||
|
const userOptions = ref<{ label: string; value: number }[]>([]);
|
||||||
|
|
||||||
|
// 选中的候选用户
|
||||||
|
const selectedUserId = ref<number | undefined>(undefined);
|
||||||
|
|
||||||
|
// 用户搜索关键词
|
||||||
|
const keyword = ref('');
|
||||||
|
|
||||||
|
// 当前角色id
|
||||||
|
const roleId = computed(() => props.data?.roleId);
|
||||||
|
|
||||||
|
// 已是成员的候选用户id
|
||||||
|
const memberIds = computed(() => new Set(members.value.map((d) => d.userId)));
|
||||||
|
|
||||||
|
/* 查询角色成员 */
|
||||||
|
const queryMembers = () => {
|
||||||
|
if (!roleId.value) {
|
||||||
|
members.value = [];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
loading.value = true;
|
||||||
|
pageRoleMembers({ roleId: roleId.value, page: 1, limit: 2000 })
|
||||||
|
.then((res) => {
|
||||||
|
loading.value = false;
|
||||||
|
members.value = res?.list ?? [];
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
loading.value = false;
|
||||||
|
members.value = [];
|
||||||
|
message.error(e.message);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/* 搜索启用用户作为候选 */
|
||||||
|
const searchUsers = (value?: string) => {
|
||||||
|
keyword.value = value ?? '';
|
||||||
|
userLoading.value = true;
|
||||||
|
pageUsers({
|
||||||
|
keywords: keyword.value || undefined,
|
||||||
|
status: 0,
|
||||||
|
page: 1,
|
||||||
|
limit: 20
|
||||||
|
})
|
||||||
|
.then((res) => {
|
||||||
|
userLoading.value = false;
|
||||||
|
userOptions.value = (res?.list ?? [])
|
||||||
|
.filter((d) => d.userId != null && !memberIds.value.has(d.userId))
|
||||||
|
.map((d) => ({
|
||||||
|
label: `${d.realName || d.nickname || d.username}(${d.username})`,
|
||||||
|
value: d.userId as number
|
||||||
|
}));
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
userLoading.value = false;
|
||||||
|
userOptions.value = [];
|
||||||
|
message.error(e.message);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/* 添加成员 */
|
||||||
|
const addOne = () => {
|
||||||
|
if (!roleId.value || selectedUserId.value == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
adding.value = true;
|
||||||
|
addRoleMember({ roleId: roleId.value, userId: selectedUserId.value })
|
||||||
|
.then((msg) => {
|
||||||
|
adding.value = false;
|
||||||
|
selectedUserId.value = undefined;
|
||||||
|
message.success(msg);
|
||||||
|
queryMembers();
|
||||||
|
searchUsers(keyword.value);
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
adding.value = false;
|
||||||
|
message.error(e.message);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/* 移除成员 */
|
||||||
|
const removeOne = (row: RoleMember) => {
|
||||||
|
if (!roleId.value || row.userId == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
removeRoleMember(roleId.value, row.userId)
|
||||||
|
.then((msg) => {
|
||||||
|
message.success(msg);
|
||||||
|
queryMembers();
|
||||||
|
searchUsers(keyword.value);
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
message.error(e.message);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/* 更新visible */
|
||||||
|
const updateVisible = (value: boolean) => {
|
||||||
|
emit('update:visible', value);
|
||||||
|
};
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.visible,
|
||||||
|
(visible) => {
|
||||||
|
if (visible) {
|
||||||
|
members.value = [];
|
||||||
|
userOptions.value = [];
|
||||||
|
selectedUserId.value = undefined;
|
||||||
|
keyword.value = '';
|
||||||
|
queryMembers();
|
||||||
|
searchUsers('');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
</script>
|
||||||
@@ -39,6 +39,8 @@
|
|||||||
<a-space>
|
<a-space>
|
||||||
<a @click="openEdit(record)">修改</a>
|
<a @click="openEdit(record)">修改</a>
|
||||||
<a-divider type="vertical" />
|
<a-divider type="vertical" />
|
||||||
|
<a @click="openMember(record)">成员</a>
|
||||||
|
<a-divider type="vertical" />
|
||||||
<a @click="openAuth(record)">分配权限</a>
|
<a @click="openAuth(record)">分配权限</a>
|
||||||
<a-divider type="vertical" />
|
<a-divider type="vertical" />
|
||||||
<a-popconfirm
|
<a-popconfirm
|
||||||
@@ -57,6 +59,8 @@
|
|||||||
<role-edit v-model:visible="showEdit" :data="current" @done="reload" />
|
<role-edit v-model:visible="showEdit" :data="current" @done="reload" />
|
||||||
<!-- 权限分配弹窗 -->
|
<!-- 权限分配弹窗 -->
|
||||||
<role-auth v-model:visible="showAuth" :data="current" />
|
<role-auth v-model:visible="showAuth" :data="current" />
|
||||||
|
<!-- 成员维护弹窗 -->
|
||||||
|
<role-member v-model:visible="showMember" :data="current" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -77,6 +81,7 @@
|
|||||||
import RoleSearch from './components/role-search.vue';
|
import RoleSearch from './components/role-search.vue';
|
||||||
import RoleEdit from './components/role-edit.vue';
|
import RoleEdit from './components/role-edit.vue';
|
||||||
import RoleAuth from './components/role-auth.vue';
|
import RoleAuth from './components/role-auth.vue';
|
||||||
|
import RoleMember from './components/role-member.vue';
|
||||||
import { pageRoles, removeRole, removeRoles } from '@/api/system/role';
|
import { pageRoles, removeRole, removeRoles } from '@/api/system/role';
|
||||||
import type { Role, RoleParam } from '@/api/system/role/model';
|
import type { Role, RoleParam } from '@/api/system/role/model';
|
||||||
|
|
||||||
@@ -120,7 +125,7 @@
|
|||||||
{
|
{
|
||||||
title: '操作',
|
title: '操作',
|
||||||
key: 'action',
|
key: 'action',
|
||||||
width: 200,
|
width: 260,
|
||||||
align: 'center'
|
align: 'center'
|
||||||
}
|
}
|
||||||
]);
|
]);
|
||||||
@@ -137,6 +142,9 @@
|
|||||||
// 是否显示权限分配弹窗
|
// 是否显示权限分配弹窗
|
||||||
const showAuth = ref(false);
|
const showAuth = ref(false);
|
||||||
|
|
||||||
|
// 是否显示成员维护弹窗
|
||||||
|
const showMember = ref(false);
|
||||||
|
|
||||||
// 表格数据源
|
// 表格数据源
|
||||||
const datasource: DatasourceFunction = ({ page, limit, where, orders }) => {
|
const datasource: DatasourceFunction = ({ page, limit, where, orders }) => {
|
||||||
return pageRoles({ ...where, ...orders, page, limit });
|
return pageRoles({ ...where, ...orders, page, limit });
|
||||||
@@ -160,6 +168,12 @@
|
|||||||
showAuth.value = true;
|
showAuth.value = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/* 打开成员维护弹窗 */
|
||||||
|
const openMember = (row?: Role) => {
|
||||||
|
current.value = row ?? null;
|
||||||
|
showMember.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
/* 删除单个 */
|
/* 删除单个 */
|
||||||
const remove = (row: Role) => {
|
const remove = (row: Role) => {
|
||||||
const hide = messageLoading('请求中..', 0);
|
const hide = messageLoading('请求中..', 0);
|
||||||
|
|||||||
@@ -255,14 +255,8 @@
|
|||||||
// trigger: 'blur'
|
// trigger: 'blur'
|
||||||
// }
|
// }
|
||||||
// ],
|
// ],
|
||||||
roles: [
|
// 角色可以为空:清空并保存表示删除该用户的全部角色关联
|
||||||
{
|
roles: [],
|
||||||
required: true,
|
|
||||||
message: '请选择角色',
|
|
||||||
type: 'array',
|
|
||||||
trigger: 'blur'
|
|
||||||
}
|
|
||||||
],
|
|
||||||
email: [
|
email: [
|
||||||
{
|
{
|
||||||
pattern: emailReg,
|
pattern: emailReg,
|
||||||
|
|||||||
Reference in New Issue
Block a user