- /approval 路由(modId 23 深层页,菜单挂载待 GetUserMod 配置) - 待办:标题/摘要(bizSnapshot 解析)/节点/到达时间,通过一键,驳回弹窗强制意见 - 办件:状态 RadioGroup 筛选(10/20/30/40),状态 Tag 着色 - 全部走本应用后端代理 /api/notice/approval/**(前端不直调 oapi,不传身份) - vite build 通过(2m24s)
This commit is contained in:
@@ -0,0 +1,83 @@
|
||||
import { Method } from "alova";
|
||||
import { alovaInstance } from "@/utils/alova/alova-iview";
|
||||
|
||||
/**
|
||||
* 应用内审批接口封装(第三步 T-C4,"我的待审核")。
|
||||
* 走本应用后端代理 /api/notice/approval/**(operator=CAS 登录态,前端不传身份);
|
||||
* 后端经网关调 flow-center,契约=BasePageResult{total,content,...}。
|
||||
*/
|
||||
|
||||
const APPROVAL = "/api/notice/approval";
|
||||
|
||||
function get<T = any>(url: string, params?: Record<string, any>): Method<T> {
|
||||
return new Method<T>("GET", alovaInstance, url, { params });
|
||||
}
|
||||
|
||||
function postJSON<T = any>(url: string, data?: Record<string, any>): Method<T> {
|
||||
return new Method<T>(
|
||||
"POST",
|
||||
alovaInstance,
|
||||
url,
|
||||
{ headers: { "Content-Type": "application/json;charset=UTF-8" } },
|
||||
data ? JSON.stringify(data) : undefined
|
||||
);
|
||||
}
|
||||
|
||||
export interface ApprovalTodoTask {
|
||||
taskId: number;
|
||||
instanceId: number;
|
||||
instanceVersion: number | null;
|
||||
bizType: string;
|
||||
bizId: string;
|
||||
bizTitle: string;
|
||||
bizSnapshot: string | null;
|
||||
nodeId: string;
|
||||
nodeName: string;
|
||||
assigneeId: number;
|
||||
assigneeName: string;
|
||||
arriveTime: string;
|
||||
}
|
||||
|
||||
export interface ApprovalInstance {
|
||||
instanceId: number;
|
||||
status: number;
|
||||
version: number;
|
||||
bizType: string;
|
||||
bizId: string;
|
||||
bizTitle: string;
|
||||
bizUrl: string | null;
|
||||
submitterName: string;
|
||||
submitTime: string;
|
||||
finishTime: string | null;
|
||||
currentNodes: string;
|
||||
}
|
||||
|
||||
export interface ApprovalPage<T> {
|
||||
total: number;
|
||||
content: T[];
|
||||
pageIndex: number;
|
||||
pageSize: number;
|
||||
totalPages: number;
|
||||
hasNext: boolean;
|
||||
}
|
||||
|
||||
export const approvalApi = {
|
||||
/** 我的待审核(bizType=notice 固定在后端) */
|
||||
todo(page = 1, size = 10): Method<ApprovalPage<ApprovalTodoTask>> {
|
||||
return get(`${APPROVAL}/Todo`, { page, size });
|
||||
},
|
||||
/** 办件视角(status 可空;10 审批中/20 完成/30 拒绝/40 撤回) */
|
||||
instances(status?: number, page = 1, size = 10): Method<ApprovalPage<ApprovalInstance>> {
|
||||
return get(`${APPROVAL}/Instances`, status ? { status, page, size } : { page, size });
|
||||
},
|
||||
/** 通过(opinion 可选) */
|
||||
approve(taskId: number, opinion?: string): Method<void> {
|
||||
return postJSON(`${APPROVAL}/Approve?taskId=${taskId}`, opinion ? { opinion } : {});
|
||||
},
|
||||
/** 拒绝(opinion 必填) */
|
||||
reject(taskId: number, opinion: string): Method<void> {
|
||||
return postJSON(`${APPROVAL}/Reject?taskId=${taskId}`, { opinion });
|
||||
},
|
||||
};
|
||||
|
||||
export default approvalApi;
|
||||
@@ -72,6 +72,13 @@ const routes: RouteRecordRaw[] = [
|
||||
component: () => import("@/views/notice/NoticeSetting.vue"),
|
||||
meta: { title: "应用设置", modId: 21 },
|
||||
},
|
||||
// 我的待审核(应用内审批;modId 23 深层页不在菜单树,菜单挂载待后端 GetUserMod 配置)
|
||||
{
|
||||
path: "/approval",
|
||||
name: "notice_approval",
|
||||
component: () => import("@/views/notice/NoticeApproval.vue"),
|
||||
meta: { title: "我的待审核", modId: 23, parentId: 2 },
|
||||
},
|
||||
|
||||
/* ---- 旧版路径兼容重定向(书签、残留的平台模块配置) ---- */
|
||||
{ path: "/accept", redirect: "/" },
|
||||
|
||||
@@ -0,0 +1,232 @@
|
||||
<template>
|
||||
<div class="approval-page">
|
||||
<Tabs v-model="tab" @on-click="load">
|
||||
<TabPane label="我的待审核" name="todo" />
|
||||
<TabPane label="办件视角" name="instances" />
|
||||
</Tabs>
|
||||
|
||||
<!-- 待审核列表 -->
|
||||
<div v-if="tab === 'todo'">
|
||||
<Table :columns="todoCols" :data="todoRows" :loading="loading" no-data-text="暂无待审核通知">
|
||||
<template #opinion="{ row }">
|
||||
<span class="opinion">{{ row._opinion }}</span>
|
||||
</template>
|
||||
<template #action="{ row }">
|
||||
<Button size="small" type="primary" :loading="acting" @click="approve(row)">通过</Button>
|
||||
<Button size="small" type="error" style="margin-left: 6px" @click="openReject(row)"
|
||||
>驳回</Button
|
||||
>
|
||||
</template>
|
||||
</Table>
|
||||
<Page
|
||||
v-if="todoPage.total > todoPage.pageSize"
|
||||
:current="todoPage.pageIndex"
|
||||
:page-size="todoPage.pageSize"
|
||||
:total="todoPage.total"
|
||||
show-total
|
||||
style="margin-top: 14px; text-align: right"
|
||||
@on-change="loadTodo"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 办件视角 -->
|
||||
<div v-else>
|
||||
<div class="filter-bar">
|
||||
<RadioGroup v-model="instStatus" type="button" @on-change="loadInstances(1)">
|
||||
<Radio :label="0">全部</Radio>
|
||||
<Radio :label="10">审批中</Radio>
|
||||
<Radio :label="20">已完成</Radio>
|
||||
<Radio :label="30">已拒绝</Radio>
|
||||
<Radio :label="40">已撤回</Radio>
|
||||
</RadioGroup>
|
||||
</div>
|
||||
<Table :columns="instCols" :data="instRows" :loading="loading" no-data-text="暂无办件">
|
||||
<template #status="{ row }">
|
||||
<Tag :color="instStatusColor(row.status)">{{ instStatusText(row.status) }}</Tag>
|
||||
</template>
|
||||
</Table>
|
||||
<Page
|
||||
v-if="instPage.total > instPage.pageSize"
|
||||
:current="instPage.pageIndex"
|
||||
:page-size="instPage.pageSize"
|
||||
:total="instPage.total"
|
||||
show-total
|
||||
style="margin-top: 14px; text-align: right"
|
||||
@on-change="loadInstances"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 驳回弹窗:意见必填 -->
|
||||
<Modal
|
||||
v-model="rejectVisible"
|
||||
title="驳回审核"
|
||||
:loading="acting"
|
||||
@on-ok="doReject"
|
||||
@on-cancel="rejectTaskId = null"
|
||||
>
|
||||
<p style="margin-bottom: 8px">
|
||||
驳回「<b>{{ rejectTitle }}</b
|
||||
>」,必须填写意见:
|
||||
</p>
|
||||
<Input v-model="rejectOpinion" type="textarea" :rows="3" placeholder="驳回意见(必填)" />
|
||||
</Modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref } from "vue";
|
||||
import { Message } from "view-ui-plus";
|
||||
import approvalApi, {
|
||||
type ApprovalInstance,
|
||||
type ApprovalTodoTask,
|
||||
} from "@/api/module/approval";
|
||||
|
||||
/**
|
||||
* 我的待审核(应用内审批,第三步):待办列表 + 办件视角,通过/驳回走本应用后端代理
|
||||
* (operator=登录态,对端按任务办理人复核;非办理人会收到业务错提示)。
|
||||
* bizSnapshot 是发起时透传的 JSON 摘要,解析失败只展示标题。
|
||||
*/
|
||||
const tab = ref<"todo" | "instances">("todo");
|
||||
const loading = ref(false);
|
||||
const acting = ref(false);
|
||||
|
||||
const todoRows = ref<(ApprovalTodoTask & { _summary: string })[]>([]);
|
||||
const todoPage = ref({ total: 0, pageIndex: 1, pageSize: 10 });
|
||||
|
||||
const instRows = ref<ApprovalInstance[]>([]);
|
||||
const instPage = ref({ total: 0, pageIndex: 1, pageSize: 10 });
|
||||
const instStatus = ref(0);
|
||||
|
||||
const rejectVisible = ref(false);
|
||||
const rejectTaskId = ref<number | null>(null);
|
||||
const rejectTitle = ref("");
|
||||
const rejectOpinion = ref("");
|
||||
|
||||
const todoCols = [
|
||||
{ title: "通知", key: "bizTitle", minWidth: 220, ellipsis: true, tooltip: true },
|
||||
{ title: "摘要", key: "_summary", minWidth: 200, ellipsis: true, tooltip: true },
|
||||
{ title: "当前节点", key: "nodeName", width: 110 },
|
||||
{ title: "到达时间", key: "arriveTime", width: 170, render: fmtTime },
|
||||
{ title: "操作", slot: "action", width: 150 },
|
||||
];
|
||||
|
||||
const instCols = [
|
||||
{ title: "通知", key: "bizTitle", minWidth: 240, ellipsis: true, tooltip: true },
|
||||
{ title: "状态", slot: "status", width: 100 },
|
||||
{ title: "提交人", key: "submitterName", width: 100 },
|
||||
{ title: "提交时间", key: "submitTime", width: 170, render: fmtTime },
|
||||
{ title: "办结时间", key: "finishTime", width: 170, render: fmtTime },
|
||||
];
|
||||
|
||||
function fmtTime(h: any, p: any) {
|
||||
return h(
|
||||
"span",
|
||||
{},
|
||||
String(p.row[p.column.key] || "").replace("T", " ").slice(0, 19) || "—"
|
||||
);
|
||||
}
|
||||
|
||||
function parseSummary(task: ApprovalTodoTask): string {
|
||||
if (!task.bizSnapshot) return "—";
|
||||
try {
|
||||
const snap = JSON.parse(task.bizSnapshot);
|
||||
return snap.summary || snap.title || "—";
|
||||
} catch {
|
||||
return "—";
|
||||
}
|
||||
}
|
||||
|
||||
async function loadTodo(page = 1) {
|
||||
loading.value = true;
|
||||
try {
|
||||
const res = await approvalApi.todo(page, todoPage.value.pageSize).send();
|
||||
todoPage.value = res;
|
||||
todoRows.value = (res.content || []).map((t) => ({ ...t, _summary: parseSummary(t) }));
|
||||
} catch (e: any) {
|
||||
Message.error(e?.message || "待审核列表加载失败");
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadInstances(page = 1) {
|
||||
loading.value = true;
|
||||
try {
|
||||
const res = await approvalApi
|
||||
.instances(instStatus.value || undefined, page, instPage.value.pageSize)
|
||||
.send();
|
||||
instPage.value = res;
|
||||
instRows.value = res.content || [];
|
||||
} catch (e: any) {
|
||||
Message.error(e?.message || "办件列表加载失败");
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function load() {
|
||||
if (tab.value === "todo") {
|
||||
loadTodo(1);
|
||||
} else {
|
||||
loadInstances(1);
|
||||
}
|
||||
}
|
||||
|
||||
async function approve(row: ApprovalTodoTask) {
|
||||
acting.value = true;
|
||||
try {
|
||||
await approvalApi.approve(row.taskId).send();
|
||||
Message.success("已通过");
|
||||
loadTodo(todoPage.value.pageIndex);
|
||||
} catch (e: any) {
|
||||
// 非办理人/已被办理等业务错,后端信封 msg 直接展示
|
||||
Message.error(e?.message || "操作失败");
|
||||
} finally {
|
||||
acting.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function openReject(row: ApprovalTodoTask) {
|
||||
rejectTaskId.value = row.taskId;
|
||||
rejectTitle.value = row.bizTitle;
|
||||
rejectOpinion.value = "";
|
||||
rejectVisible.value = true;
|
||||
}
|
||||
|
||||
async function doReject() {
|
||||
if (!rejectOpinion.value.trim()) {
|
||||
Message.warning("驳回必须填写意见");
|
||||
return;
|
||||
}
|
||||
acting.value = true;
|
||||
try {
|
||||
await approvalApi.reject(rejectTaskId.value!, rejectOpinion.value.trim()).send();
|
||||
Message.success("已驳回");
|
||||
rejectVisible.value = false;
|
||||
loadTodo(todoPage.value.pageIndex);
|
||||
} catch (e: any) {
|
||||
Message.error(e?.message || "操作失败");
|
||||
} finally {
|
||||
acting.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function instStatusText(s: number) {
|
||||
return ({ 10: "审批中", 20: "已完成", 30: "已拒绝", 40: "已撤回" } as Record<number, string>)[s] || String(s);
|
||||
}
|
||||
|
||||
function instStatusColor(s: number) {
|
||||
return ({ 10: "blue", 20: "green", 30: "red", 40: "default" } as Record<number, string>)[s] || "default";
|
||||
}
|
||||
|
||||
onMounted(load);
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.approval-page {
|
||||
padding: 12px 0;
|
||||
}
|
||||
.filter-bar {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user