- /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"),
|
component: () => import("@/views/notice/NoticeSetting.vue"),
|
||||||
meta: { title: "应用设置", modId: 21 },
|
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: "/" },
|
{ 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