This commit is contained in:
hero920103 committed 2026-10-17 11:52:02 +08:00
1 parent 184d5b0403
commit a758966fae
18 files changed
+2002 -19

No files matched your search

+27
View File
@@ -57,3 +57,30 @@ export function getApprovalFlowPick(params) {
export function getAvailableFlows(params) {
return request({ url: '/backend/approval/flow/available', method: 'get', params });
}
// ==================== 审批中心 ====================
// 我的任务列表:type=pending 待我审批 / done 我已审批 / cc 抄送我的
export function getCenterTaskList(params) {
return request({ url: '/backend/approval/center/task/list', method: 'get', params });
}
// 通用审批操作:{ task_id, pass, comment }
export function actCenterTask(data) {
return request({ url: '/backend/approval/center/task/act', method: 'post', data });
}
// 我发起的审批实例列表(status 可选:1审批中 2通过 3驳回 4撤回)
export function getCenterInstanceList(params) {
return request({ url: '/backend/approval/center/instance/list', method: 'get', params });
}
// 审批实例详情(含节点任务时间线)
export function getCenterInstanceDetail(id) {
return request({ url: `/backend/approval/center/instance/detail/${id}`, method: 'get' });
}
// 审批权限设置:只更新流程的适用部门/是否默认/启用状态(body: {id, apply_dept_ids, is_default, status})
export function saveApprovalFlowPerm(data) {
return request({ url: '/backend/approval/flow/perm', method: 'post', data });
}
+19
View File
@@ -22,6 +22,25 @@ const staticMainChildren = [
component: () => import("@/views/system/approval/index.vue"),
meta: { requiresAuth: true, title: "审批流", modulePath: "/systemManager" }
},
// OA 审批中心:待我审批 / 审批列表 / 审批设置(静态注册保证 URL 直达)
{
path: "/apps/oa/approvalcenter/mine",
name: "OaApprovalCenterMine",
component: () => import("@/views/apps/oa/approvalcenter/mine.vue"),
meta: { requiresAuth: true, title: "待我审批", modulePath: "/apps/oa" }
},
{
path: "/apps/oa/approvalcenter/list",
name: "OaApprovalCenterList",
component: () => import("@/views/apps/oa/approvalcenter/list.vue"),
meta: { requiresAuth: true, title: "审批列表", modulePath: "/apps/oa" }
},
{
path: "/apps/oa/approvalcenter/settings",
name: "OaApprovalCenterSettings",
component: () => import("@/views/apps/oa/approvalcenter/settings.vue"),
meta: { requiresAuth: true, title: "审批设置", modulePath: "/apps/oa" }
},
// CMS 文章/产品/解决方案是系统内置功能,不依赖数据库菜单配置。
// 统一结构:index 为列表页,type 为分类页。
{
+3 -2
View File
@@ -7,7 +7,7 @@
* 环境变量(见 .env.development / .env.production):
* VITE_AUTH_BASE=https://api.yunzer.cn/auth 认证中心地址,缺省回退到 ${VITE_API_BASE_URL}/auth
* VITE_AUTH_CLIENT_ID=yz-backend
* VITE_AUTH_REDIRECT_URI=https://back.yunzer.cn/#/auth/callback
* VITE_AUTH_REDIRECT_URI=https://back.yunzer.cn/auth-callback.html
* VITE_AUTH_POST_LOGOUT_URI=https://back.yunzer.cn/#/
*
* 说明:token 仍存在 localStorage(沿用现有 utils/request.js 与 stores/auth.js),
@@ -21,8 +21,9 @@ const AUTH_BASE = (import.meta.env.VITE_AUTH_BASE || (API_BASE ? `${API_BASE}/au
""
);
const CLIENT_ID = import.meta.env.VITE_AUTH_CLIENT_ID || "yz-backend";
// 缺省回落到静态中转页(不带 #);该地址必须已在认证中心白名单登记
const REDIRECT_URI =
import.meta.env.VITE_AUTH_REDIRECT_URI || `${window.location.origin}/#/auth/callback`;
import.meta.env.VITE_AUTH_REDIRECT_URI || `${window.location.origin}/auth-callback.html`;
const POST_LOGOUT_URI =
import.meta.env.VITE_AUTH_POST_LOGOUT_URI || `${window.location.origin}/#/`;
@@ -0,0 +1,159 @@
<template>
<el-dialog
v-model="visible"
title="审批进度"
width="560px"
:close-on-click-modal="false"
append-to-body
>
<div v-loading="loading" class="progress-wrap">
<template v-if="detail">
<div class="progress-head">
<div class="progress-title">
<span class="title-text">{{ detail.biz_title }}</span>
<el-tag :type="instanceStatusType(detail.status)" size="small">
{{ instanceStatusLabel(detail.status) }}
</el-tag>
</div>
<div class="progress-meta">
<span v-if="detail.flow_name">流程:{{ detail.flow_name }}</span>
<span>发起人:{{ detail.submit_user_name || "-" }}</span>
<span>发起时间:{{ detail.submit_time || "-" }}</span>
<span v-if="detail.finish_time">完成时间:{{ detail.finish_time }}</span>
</div>
</div>
<el-timeline class="progress-timeline">
<el-timeline-item
v-for="node in detail.timeline"
:key="node.task_id"
:type="timelineDotType(node.status)"
:timestamp="node.finish_time || ''"
>
<div class="timeline-node">
<div class="node-head">
<span class="node-name">{{ node.node_name }}</span>
<el-tag :type="taskStatusType(node.status)" size="small" effect="plain">
{{ taskStatusLabel(node.status) }}
</el-tag>
</div>
<div class="node-approver">处理人:{{ node.approver || "-" }}</div>
<div v-if="node.comment" class="node-comment">意见:{{ node.comment }}</div>
</div>
</el-timeline-item>
</el-timeline>
<el-empty v-if="!detail.timeline?.length" description="暂无审批记录" :image-size="80" />
</template>
</div>
</el-dialog>
</template>
<script setup>
import { computed, ref, watch } from "vue";
import { ElMessage } from "element-plus";
import { getCenterInstanceDetail } from "@/api/approval";
const props = defineProps({
modelValue: { type: Boolean, default: false },
instanceId: { type: [Number, String], default: 0 },
});
const emit = defineEmits(["update:modelValue"]);
const visible = computed({
get: () => props.modelValue,
set: (val) => emit("update:modelValue", val),
});
const loading = ref(false);
const detail = ref(null);
// 实例状态:1审批中 2通过 3驳回 4撤回 5取消
const instanceStatusLabel = (status) =>
({ 1: "审批中", 2: "已通过", 3: "已驳回", 4: "已撤回", 5: "已取消" }[status] || "未知");
const instanceStatusType = (status) =>
({ 1: "warning", 2: "success", 3: "danger", 4: "info", 5: "info" }[status] || "info");
// 任务状态:1待审批 2已通过 3已驳回 6已通知(抄送)
const taskStatusLabel = (status) =>
({ 1: "待审批", 2: "已通过", 3: "已驳回", 6: "已通知" }[status] || "未知");
const taskStatusType = (status) =>
({ 1: "warning", 2: "success", 3: "danger", 6: "info" }[status] || "info");
const timelineDotType = (status) => taskStatusType(status);
watch(
() => [props.modelValue, props.instanceId],
([open, id]) => {
if (open && id) loadDetail(id);
},
{ immediate: true },
);
const loadDetail = async (id) => {
loading.value = true;
detail.value = null;
try {
const res = await getCenterInstanceDetail(id);
detail.value = res?.data?.data ?? res?.data ?? null;
} catch (error) {
ElMessage.error(error?.message || "加载审批进度失败");
} finally {
loading.value = false;
}
};
</script>
<style lang="less" scoped>
.progress-wrap {
min-height: 160px;
}
.progress-head {
margin-bottom: 16px;
padding-bottom: 12px;
border-bottom: 1px solid var(--el-border-color-lighter);
}
.progress-title {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 8px;
.title-text {
font-size: 15px;
font-weight: 600;
color: var(--el-text-color-primary);
}
}
.progress-meta {
display: flex;
flex-wrap: wrap;
gap: 4px 16px;
font-size: 12px;
color: var(--el-text-color-secondary);
}
.progress-timeline {
padding-left: 4px;
}
.timeline-node {
.node-head {
display: flex;
align-items: center;
gap: 8px;
}
.node-name {
font-weight: 600;
color: var(--el-text-color-primary);
}
.node-approver,
.node-comment {
margin-top: 4px;
font-size: 12px;
color: var(--el-text-color-secondary);
}
}
</style>
@@ -0,0 +1,312 @@
<template>
<div class="approval-center-page">
<div class="page-header">
<div>
<h2>审批列表</h2>
<p>查看我参与的全部审批记录</p>
</div>
</div>
<div class="table-card">
<el-tabs v-model="activeTab" @tab-change="handleTabChange">
<!-- 我已审批 -->
<el-tab-pane label="我已审批" name="done">
<div class="table-scroll">
<el-table v-loading="loading" :data="taskList" stripe>
<el-table-column prop="biz_title" label="审批事项" min-width="220" show-overflow-tooltip />
<el-table-column prop="biz_type" label="类型" width="100">
<template #default="{ row }">
<el-tag size="small" effect="plain">{{ bizTypeLabel(row.biz_type) }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="submit_user_name" label="发起人" width="120">
<template #default="{ row }">{{ row.submit_user_name || "-" }}</template>
</el-table-column>
<el-table-column prop="node_name" label="处理节点" width="140">
<template #default="{ row }">{{ row.node_name || "-" }}</template>
</el-table-column>
<el-table-column prop="status" label="我的处理" width="110">
<template #default="{ row }">
<el-tag :type="taskStatusType(row.status)" size="small">
{{ taskStatusLabel(row.status) }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="comment" label="审批意见" min-width="140" show-overflow-tooltip>
<template #default="{ row }">{{ row.comment || "-" }}</template>
</el-table-column>
<el-table-column prop="finish_time" label="处理时间" width="150">
<template #default="{ row }">{{ row.finish_time || "-" }}</template>
</el-table-column>
<el-table-column label="操作" fixed="right" width="90">
<template #default="{ row }">
<el-button link type="primary" @click="openProgress(row)">进度</el-button>
</template>
</el-table-column>
<template #empty><el-empty description="暂无已审批记录" /></template>
</el-table>
</div>
</el-tab-pane>
<!-- 我发起的 -->
<el-tab-pane label="我发起的" name="submitted">
<div class="tab-filter">
<el-select v-model="instanceStatus" clearable placeholder="全部状态" class="status-select" @change="loadInstances">
<el-option label="审批中" :value="1" />
<el-option label="已通过" :value="2" />
<el-option label="已驳回" :value="3" />
<el-option label="已撤回" :value="4" />
</el-select>
</div>
<div class="table-scroll">
<el-table v-loading="loading" :data="instanceList" stripe>
<el-table-column prop="biz_title" label="审批事项" min-width="220" show-overflow-tooltip />
<el-table-column prop="flow_name" label="所属流程" min-width="140" show-overflow-tooltip>
<template #default="{ row }">{{ row.flow_name || "-" }}</template>
</el-table-column>
<el-table-column prop="biz_type" label="类型" width="100">
<template #default="{ row }">
<el-tag size="small" effect="plain">{{ bizTypeLabel(row.biz_type) }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="status" label="状态" width="100">
<template #default="{ row }">
<el-tag :type="instanceStatusType(row.status)" size="small">
{{ instanceStatusLabel(row.status) }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="submit_time" label="发起时间" width="150">
<template #default="{ row }">{{ row.submit_time || "-" }}</template>
</el-table-column>
<el-table-column prop="finish_time" label="完成时间" width="150">
<template #default="{ row }">{{ row.finish_time || "-" }}</template>
</el-table-column>
<el-table-column label="操作" fixed="right" width="90">
<template #default="{ row }">
<el-button link type="primary" @click="openProgress(row)">进度</el-button>
</template>
</el-table-column>
<template #empty><el-empty description="暂无发起的审批" /></template>
</el-table>
</div>
</el-tab-pane>
<!-- 抄送我的 -->
<el-tab-pane label="抄送我的" name="cc">
<div class="table-scroll">
<el-table v-loading="loading" :data="taskList" stripe>
<el-table-column prop="biz_title" label="审批事项" min-width="220" show-overflow-tooltip />
<el-table-column prop="biz_type" label="类型" width="100">
<template #default="{ row }">
<el-tag size="small" effect="plain">{{ bizTypeLabel(row.biz_type) }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="submit_user_name" label="发起人" width="120">
<template #default="{ row }">{{ row.submit_user_name || "-" }}</template>
</el-table-column>
<el-table-column prop="node_name" label="通知节点" width="140">
<template #default="{ row }">{{ row.node_name || "-" }}</template>
</el-table-column>
<el-table-column prop="finish_time" label="通知时间" width="150">
<template #default="{ row }">{{ row.finish_time || "-" }}</template>
</el-table-column>
<el-table-column label="操作" fixed="right" width="90">
<template #default="{ row }">
<el-button link type="primary" @click="openProgress(row)">进度</el-button>
</template>
</el-table-column>
<template #empty><el-empty description="暂无抄送记录" /></template>
</el-table>
</div>
</el-tab-pane>
</el-tabs>
<div class="pagination">
<el-pagination
:current-page="pagination.page"
:page-size="pagination.pageSize"
:total="pagination.total"
:layout="isMobile ? 'prev, pager, next' : 'total, sizes, prev, pager, next, jumper'"
@update:current-page="pagination.page = $event"
@update:page-size="pagination.pageSize = $event"
@current-change="loadCurrent"
@size-change="loadCurrent"
/>
</div>
</div>
<ProgressTimeline v-model="progressVisible" :instance-id="progressInstanceId" />
</div>
</template>
<script setup>
import { onMounted, reactive, ref } from "vue";
import { ElMessage } from "element-plus";
import ProgressTimeline from "./components/ProgressTimeline.vue";
import { getCenterInstanceList, getCenterTaskList } from "@/api/approval";
const activeTab = ref("done");
const taskList = ref([]);
const instanceList = ref([]);
const instanceStatus = ref(undefined);
const loading = ref(false);
const pagination = reactive({ page: 1, pageSize: 10, total: 0 });
const progressVisible = ref(false);
const progressInstanceId = ref(0);
const isMobile = ref(false);
const checkMobile = () => {
isMobile.value = window.innerWidth <= 768;
};
const responseData = (res) => res?.data?.data ?? res?.data ?? res ?? {};
const bizTypeLabel = (bizType) =>
({ reimburse: "报销", oa: "OA 审批" }[bizType] || bizType || "-");
// 任务状态:1待审批 2已通过 3已驳回 6已通知
const taskStatusLabel = (status) =>
({ 1: "待审批", 2: "已通过", 3: "已驳回", 6: "已通知" }[status] || "未知");
const taskStatusType = (status) =>
({ 1: "warning", 2: "success", 3: "danger", 6: "info" }[status] || "info");
// 实例状态:1审批中 2通过 3驳回 4撤回
const instanceStatusLabel = (status) =>
({ 1: "审批中", 2: "已通过", 3: "已驳回", 4: "已撤回" }[status] || "未知");
const instanceStatusType = (status) =>
({ 1: "warning", 2: "success", 3: "danger", 4: "info" }[status] || "info");
const loadTasks = async (type) => {
loading.value = true;
try {
const data = responseData(
await getCenterTaskList({ type, ...pagination }),
);
taskList.value = data.list || [];
pagination.total = Number(data.total || 0);
} catch (error) {
ElMessage.error(error?.message || "加载审批列表失败");
} finally {
loading.value = false;
}
};
const loadInstances = async () => {
loading.value = true;
try {
const data = responseData(
await getCenterInstanceList({
status: instanceStatus.value ?? undefined,
...pagination,
}),
);
instanceList.value = data.list || [];
pagination.total = Number(data.total || 0);
} catch (error) {
ElMessage.error(error?.message || "加载审批列表失败");
} finally {
loading.value = false;
}
};
const loadCurrent = () => {
if (activeTab.value === "submitted") loadInstances();
else loadTasks(activeTab.value);
};
const handleTabChange = () => {
pagination.page = 1;
pagination.total = 0;
taskList.value = [];
instanceList.value = [];
loadCurrent();
};
const openProgress = (row) => {
progressInstanceId.value = row.instance_id || row.id;
progressVisible.value = true;
};
onMounted(() => {
checkMobile();
window.addEventListener("resize", checkMobile);
loadCurrent();
});
</script>
<style lang="less" scoped>
.approval-center-page {
padding: 24px;
}
.page-header {
display: flex;
align-items: flex-start;
justify-content: space-between;
margin-bottom: 16px;
h2 {
margin: 0 0 4px;
font-size: 20px;
color: var(--el-text-color-primary);
}
p {
margin: 0;
font-size: 13px;
color: var(--el-text-color-secondary);
}
}
.table-card {
padding: 8px 16px 16px;
background: var(--el-bg-color);
border-radius: 8px;
}
.tab-filter {
display: flex;
justify-content: flex-end;
margin-bottom: 12px;
.status-select {
width: 160px;
}
}
.pagination {
display: flex;
justify-content: flex-end;
margin-top: 18px;
}
@media (max-width: 768px) {
.approval-center-page {
padding: 12px;
}
.page-header {
flex-direction: column;
gap: 12px;
}
.table-card {
padding: 4px 12px 12px;
}
.table-scroll {
overflow-x: auto;
-webkit-overflow-scrolling: touch;
:deep(.el-table) {
min-width: 720px;
}
}
.pagination {
justify-content: center;
margin-top: 12px;
}
}
</style>
@@ -0,0 +1,230 @@
<template>
<div class="approval-center-page">
<div class="page-header">
<div>
<h2>待我审批</h2>
<p>处理与我相关的 OA 审批事项</p>
</div>
<div class="header-actions">
<el-badge :value="pagination.total" :hidden="!pagination.total" type="warning">
<el-tag type="warning" effect="plain">待办 {{ pagination.total }} 条</el-tag>
</el-badge>
</div>
</div>
<div class="table-card">
<div class="table-scroll">
<el-table v-loading="loading" :data="list" stripe>
<el-table-column
prop="biz_title"
label="审批事项"
min-width="220"
show-overflow-tooltip
/>
<el-table-column prop="biz_type" label="类型" width="100">
<template #default="{ row }">
<el-tag size="small" effect="plain">{{ bizTypeLabel(row.biz_type) }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="submit_user_name" label="发起人" width="120">
<template #default="{ row }">{{ row.submit_user_name || "-" }}</template>
</el-table-column>
<el-table-column prop="submit_time" label="发起时间" width="150">
<template #default="{ row }">{{ row.submit_time || "-" }}</template>
</el-table-column>
<el-table-column prop="node_name" label="审批节点" width="140">
<template #default="{ row }">{{ row.node_name || "-" }}</template>
</el-table-column>
<el-table-column prop="deadline" label="处理时限" width="150">
<template #default="{ row }">
<span :class="{ overdue: isOverdue(row.deadline) }">{{
row.deadline || "-"
}}</span>
</template>
</el-table-column>
<el-table-column label="操作" fixed="right" width="180">
<template #default="{ row }">
<el-button link type="success" @click="handleAct(row, true)">通过</el-button>
<el-button link type="danger" @click="handleAct(row, false)">驳回</el-button>
<el-button link type="primary" @click="openProgress(row)">进度</el-button>
</template>
</el-table-column>
<template #empty><el-empty description="暂无待审批事项" /></template>
</el-table>
</div>
<div class="pagination">
<el-pagination
:current-page="pagination.page"
:page-size="pagination.pageSize"
:total="pagination.total"
:layout="isMobile ? 'prev, pager, next' : 'total, sizes, prev, pager, next, jumper'"
@update:current-page="pagination.page = $event"
@update:page-size="pagination.pageSize = $event"
@current-change="loadList"
@size-change="loadList"
/>
</div>
</div>
<ProgressTimeline v-model="progressVisible" :instance-id="progressInstanceId" />
</div>
</template>
<script setup>
import { onMounted, reactive, ref } from "vue";
import { ElMessage, ElMessageBox } from "element-plus";
import ProgressTimeline from "./components/ProgressTimeline.vue";
import { actCenterTask, getCenterTaskList } from "@/api/approval";
const list = ref([]);
const loading = ref(false);
const pagination = reactive({ page: 1, pageSize: 10, total: 0 });
const progressVisible = ref(false);
const progressInstanceId = ref(0);
const isMobile = ref(false);
const checkMobile = () => {
isMobile.value = window.innerWidth <= 768;
};
const responseData = (res) => res?.data?.data ?? res?.data ?? res ?? {};
// 业务类型中文名(与流程配置的分类/单据类型对应)
const bizTypeLabel = (bizType) =>
({ reimburse: "报销", oa: "OA 审批" }[bizType] || bizType || "-");
const isOverdue = (deadline) => {
if (!deadline) return false;
return new Date(deadline.replace(/-/g, "/")).getTime() < Date.now();
};
const loadList = async () => {
loading.value = true;
try {
const data = responseData(
await getCenterTaskList({ type: "pending", ...pagination }),
);
list.value = data.list || [];
pagination.total = Number(data.total || 0);
} catch (error) {
ElMessage.error(error?.message || "加载待办列表失败");
} finally {
loading.value = false;
}
};
// 通过/驳回:驳回时填写审批意见
const handleAct = (row, pass) => {
const doAct = async (comment) => {
try {
const res = responseData(await actCenterTask({ task_id: row.task_id, pass, comment }));
ElMessage.success(res?.message || (pass ? "已通过" : "已驳回"));
loadList();
} catch (error) {
ElMessage.error(error?.message || "操作失败");
}
};
if (pass) {
ElMessageBox.confirm(`确认通过「${row.biz_title}」的「${row.node_name}」节点吗?`, "通过审批", {
type: "success",
confirmButtonText: "通过",
cancelButtonText: "取消",
}).then(() => doAct(""));
} else {
ElMessageBox.prompt("请输入驳回原因", "驳回", {
type: "warning",
inputPlaceholder: "驳回原因(选填)",
confirmButtonText: "驳回",
cancelButtonText: "取消",
}).then(({ value }) => doAct((value || "").trim()));
}
};
const openProgress = (row) => {
progressInstanceId.value = row.instance_id;
progressVisible.value = true;
};
onMounted(() => {
checkMobile();
window.addEventListener("resize", checkMobile);
loadList();
});
</script>
<style lang="less" scoped>
.approval-center-page {
padding: 24px;
}
.page-header {
display: flex;
align-items: flex-start;
justify-content: space-between;
margin-bottom: 16px;
h2 {
margin: 0 0 4px;
font-size: 20px;
color: var(--el-text-color-primary);
}
p {
margin: 0;
font-size: 13px;
color: var(--el-text-color-secondary);
}
}
.header-actions {
display: flex;
align-items: center;
}
.overdue {
color: var(--el-color-danger);
font-weight: 600;
}
.table-card {
padding: 16px;
background: var(--el-bg-color);
border-radius: 8px;
}
.pagination {
display: flex;
justify-content: flex-end;
margin-top: 18px;
}
@media (max-width: 768px) {
.approval-center-page {
padding: 12px;
}
.page-header {
flex-direction: column;
gap: 12px;
}
.table-card {
padding: 12px;
}
.table-scroll {
overflow-x: auto;
-webkit-overflow-scrolling: touch;
:deep(.el-table) {
min-width: 760px;
}
}
.pagination {
justify-content: center;
margin-top: 12px;
}
}
</style>
@@ -0,0 +1,297 @@
<template>
<div class="approval-center-page">
<div class="page-header">
<div>
<h2>审批设置</h2>
<p>配置 OA 各业务的审批流程模板、表单与权限</p>
</div>
<div class="header-actions">
<el-button type="primary" :icon="Plus" @click="goDesigner">新建流程</el-button>
</div>
</div>
<div class="table-card">
<el-tabs v-model="activeTab">
<!-- 流程模板 -->
<el-tab-pane label="流程模板" name="flows">
<el-table v-loading="flowsLoading" :data="flows" stripe>
<el-table-column prop="flow_code" label="流程编号" width="120" />
<el-table-column prop="flow_name" label="流程名称" min-width="160" show-overflow-tooltip />
<el-table-column prop="module_code" label="所属模块" width="120">
<template #default="{ row }">{{ moduleName(row.module_code) }}</template>
</el-table-column>
<el-table-column prop="biz_type" label="适用类型" width="110">
<template #default="{ row }">{{ bizTypeLabel(row.biz_type) }}</template>
</el-table-column>
<el-table-column prop="node_count" label="节点数" width="80" align="center" />
<el-table-column prop="is_default" label="默认流" width="90" align="center">
<template #default="{ row }">
<el-tag v-if="row.is_default === 1" type="success" size="small">默认</el-tag>
<span v-else>-</span>
</template>
</el-table-column>
<el-table-column prop="status" label="状态" width="90">
<template #default="{ row }">
<el-switch
:model-value="row.status === 1"
@change="(val) => toggleFlowStatus(row, val)"
/>
</template>
</el-table-column>
<el-table-column label="操作" fixed="right" width="140">
<template #default="{ row }">
<el-button link type="primary" @click="goDesigner">编辑</el-button>
<el-button link type="danger" @click="handleDeleteFlow(row)">删除</el-button>
</template>
</el-table-column>
<template #empty><el-empty description="暂无审批流程,点击右上角新建" /></template>
</el-table>
</el-tab-pane>
<!-- 表单设计 -->
<el-tab-pane label="表单设计" name="forms">
<el-empty description="当前报销等业务使用内置固定表单,自定义表单设计器规划中">
<template #image>
<el-icon :size="48" color="var(--el-color-info)"><Document /></el-icon>
</template>
</el-empty>
</el-tab-pane>
<!-- 审批权限 -->
<el-tab-pane label="审批权限" name="perms">
<el-table v-loading="flowsLoading" :data="flows" stripe>
<el-table-column prop="flow_name" label="流程名称" min-width="150" show-overflow-tooltip />
<el-table-column prop="biz_type" label="适用类型" width="110">
<template #default="{ row }">{{ bizTypeLabel(row.biz_type) }}</template>
</el-table-column>
<el-table-column label="适用部门" min-width="240">
<template #default="{ row }">
<el-select
v-model="row._dept_ids"
multiple
collapse-tags
clearable
placeholder="全部部门(全公司通用)"
class="dept-select"
>
<el-option
v-for="org in orgs"
:key="org.id"
:label="org.name"
:value="org.id"
/>
</el-select>
</template>
</el-table-column>
<el-table-column label="默认流" width="90" align="center">
<template #default="{ row }">
<el-switch
:model-value="row.is_default === 1"
@change="(val) => savePerm(row, { is_default: val ? 1 : 0 })"
/>
</template>
</el-table-column>
<el-table-column label="操作" fixed="right" width="100">
<template #default="{ row }">
<el-button
link
type="primary"
:loading="savingPermId === row.id"
@click="savePerm(row, { apply_dept_ids: row._dept_ids })"
>保存部门</el-button
>
</template>
</el-table-column>
<template #empty><el-empty description="暂无审批流程" /></template>
</el-table>
<div class="perm-tip">
适用部门为空表示全公司通用;同一业务类型下仅一条默认流程,发起时自动命中。
</div>
</el-tab-pane>
</el-tabs>
</div>
</div>
</template>
<script setup>
import { onMounted, ref } from "vue";
import { useRouter } from "vue-router";
import { ElMessage, ElMessageBox } from "element-plus";
import { Plus, Document } from "@element-plus/icons-vue";
import {
deleteApprovalFlow,
getApprovalFlowList,
getApprovalOptions,
saveApprovalFlowPerm,
} from "@/api/approval";
const router = useRouter();
const activeTab = ref("flows");
const flows = ref([]);
const orgs = ref([]);
const modules = ref([]);
const flowsLoading = ref(false);
const savingPermId = ref(0);
const responseData = (res) => res?.data?.data ?? res?.data ?? res ?? {};
const moduleName = (code) =>
modules.value.find((m) => m.code === code)?.name || code || "-";
const bizTypeLabel = (bizType) =>
({ reimburse: "报销", oa: "OA 通用" }[bizType] || bizType || "-");
const loadOptions = async () => {
try {
const data = responseData(await getApprovalOptions());
orgs.value = data?.orgs || [];
modules.value = data?.modules || [];
} catch (error) {
ElMessage.error(error?.message || "加载配置数据失败");
}
};
const loadFlows = async () => {
flowsLoading.value = true;
try {
const data = responseData(await getApprovalFlowList());
const rows = Array.isArray(data) ? data : data?.list || [];
// _dept_ids 为本地编辑态(后端 apply_dept_ids 即 ID 数组)
flows.value = rows.map((row) => ({
...row,
_dept_ids: Array.isArray(row.apply_dept_ids) ? row.apply_dept_ids : [],
}));
} catch (error) {
ElMessage.error(error?.message || "加载流程列表失败");
} finally {
flowsLoading.value = false;
}
};
const goDesigner = () => {
router.push("/system/approval");
};
// 启停流程(复用权限接口,仅更新状态字段)
const toggleFlowStatus = async (row, enabled) => {
try {
await saveApprovalFlowPerm({
id: row.id,
apply_dept_ids: row._dept_ids,
is_default: row.is_default,
status: enabled ? 1 : 0,
});
row.status = enabled ? 1 : 0;
ElMessage.success(enabled ? "已启用" : "已停用");
} catch (error) {
ElMessage.error(error?.message || "操作失败");
}
};
const savePerm = async (row, patch) => {
savingPermId.value = row.id;
try {
await saveApprovalFlowPerm({
id: row.id,
apply_dept_ids: patch.apply_dept_ids ?? row._dept_ids,
is_default: patch.is_default ?? row.is_default,
status: row.status,
});
if (patch.is_default !== undefined) row.is_default = patch.is_default;
ElMessage.success("保存成功");
loadFlows();
} catch (error) {
ElMessage.error(error?.message || "保存失败");
} finally {
savingPermId.value = 0;
}
};
const handleDeleteFlow = (row) => {
ElMessageBox.confirm(
`确定删除流程「${row.flow_name}」吗?在途审批不受影响,但该流程将无法再被发起。`,
"删除流程",
{ type: "warning", confirmButtonText: "删除", cancelButtonText: "取消" },
)
.then(async () => {
try {
await deleteApprovalFlow(row.id);
ElMessage.success("已删除");
loadFlows();
} catch (error) {
ElMessage.error(error?.message || "删除失败");
}
})
.catch(() => {});
};
onMounted(() => {
loadOptions();
loadFlows();
});
</script>
<style lang="less" scoped>
.approval-center-page {
padding: 24px;
}
.page-header {
display: flex;
align-items: flex-start;
justify-content: space-between;
margin-bottom: 16px;
h2 {
margin: 0 0 4px;
font-size: 20px;
color: var(--el-text-color-primary);
}
p {
margin: 0;
font-size: 13px;
color: var(--el-text-color-secondary);
}
}
.table-card {
padding: 8px 16px 16px;
background: var(--el-bg-color);
border-radius: 8px;
}
.dept-select {
width: 100%;
max-width: 320px;
}
.perm-tip {
margin-top: 12px;
font-size: 12px;
color: var(--el-text-color-secondary);
}
@media (max-width: 768px) {
.approval-center-page {
padding: 12px;
}
.page-header {
flex-direction: column;
gap: 12px;
}
.table-card {
padding: 4px 12px 12px;
}
.table-scroll,
:deep(.el-table) {
overflow-x: auto;
}
:deep(.el-table) {
min-width: 680px;
}
}
</style>