feat(approval): 我的待审核页——待办列表+办件视角+通过/驳回
build / build (push) Successful in 2m41s

- /approval 路由(modId 23 深层页,菜单挂载待 GetUserMod 配置)
- 待办:标题/摘要(bizSnapshot 解析)/节点/到达时间,通过一键,驳回弹窗强制意见
- 办件:状态 RadioGroup 筛选(10/20/30/40),状态 Tag 着色
- 全部走本应用后端代理 /api/notice/approval/**(前端不直调 oapi,不传身份)
- vite build 通过(2m24s)
This commit is contained in:
chenxing
2026-09-23 00:36:21 +08:00
parent 468cb8670f
commit f41719f3c4
3 changed files with 322 additions and 0 deletions
+83
View File
@@ -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;
+7
View File
@@ -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: "/" },
+232
View File
@@ -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>