Merge branch 'master' of https://git.yunzer.cn/yunzerwebsite/yunzerwebsiteallinone
This commit is contained in:
commit
699a117678
28 files changed
+4428
-970
No files matched your search
+16
-16
@@ -58,29 +58,29 @@ 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 });
|
||||
// 我发起的审批实例列表
|
||||
export function getMyApprovalInstances(params) {
|
||||
return request({ url: '/backend/approval/instance/mine', method: 'get', params });
|
||||
}
|
||||
|
||||
// 通用审批操作:{ task_id, pass, comment }
|
||||
export function actCenterTask(data) {
|
||||
return request({ url: '/backend/approval/center/task/act', method: 'post', data });
|
||||
// 我作为审批人的任务列表(handled=1 返回我已处理)
|
||||
export function getApprovalTodo(params) {
|
||||
return request({ url: '/backend/approval/instance/todo', method: 'get', params });
|
||||
}
|
||||
|
||||
// 我发起的审批实例列表(status 可选:1审批中 2通过 3驳回 4撤回)
|
||||
export function getCenterInstanceList(params) {
|
||||
return request({ url: '/backend/approval/center/instance/list', method: 'get', params });
|
||||
// 审批实例详情(含流程时间线)
|
||||
export function getApprovalInstanceDetail(id) {
|
||||
return request({ url: `/backend/approval/instance/detail/${id}`, method: 'get' });
|
||||
}
|
||||
|
||||
// 审批实例详情(含节点任务时间线)
|
||||
export function getCenterInstanceDetail(id) {
|
||||
return request({ url: `/backend/approval/center/instance/detail/${id}`, method: 'get' });
|
||||
// 审批人通过 / 驳回
|
||||
export function approveApprovalInstance(data) {
|
||||
return request({ url: '/backend/approval/instance/approve', method: 'post', data });
|
||||
}
|
||||
|
||||
// 审批权限设置:只更新流程的适用部门/是否默认/启用状态(body: {id, apply_dept_ids, is_default, status})
|
||||
export function saveApprovalFlowPerm(data) {
|
||||
return request({ url: '/backend/approval/flow/perm', method: 'post', data });
|
||||
// 发起人撤回
|
||||
export function withdrawApprovalInstance(id) {
|
||||
return request({ url: `/backend/approval/instance/withdraw/${id}`, method: 'post' });
|
||||
}
|
||||
@@ -0,0 +1,383 @@
|
||||
<template>
|
||||
<el-drawer v-model="visible" title="审批详情" size="560px" destroy-on-close>
|
||||
<div v-loading="loading">
|
||||
<template v-if="item">
|
||||
<el-descriptions :column="1" border size="small">
|
||||
<el-descriptions-item label="单据标题">{{
|
||||
item.biz_title || "-"
|
||||
}}</el-descriptions-item>
|
||||
<el-descriptions-item label="单据类型">{{
|
||||
item.biz_type || "-"
|
||||
}}</el-descriptions-item>
|
||||
<el-descriptions-item label="审批流程">{{
|
||||
flowName || "-"
|
||||
}}</el-descriptions-item>
|
||||
<el-descriptions-item label="发起人">{{
|
||||
item.submit_user_name || "-"
|
||||
}}</el-descriptions-item>
|
||||
<el-descriptions-item label="提交时间">{{
|
||||
item.submit_time || item.create_time || "-"
|
||||
}}</el-descriptions-item>
|
||||
<el-descriptions-item label="完成时间">{{
|
||||
item.finish_time || "-"
|
||||
}}</el-descriptions-item>
|
||||
<el-descriptions-item label="当前状态">
|
||||
<el-tag :type="tagType(item.status_tag)">{{
|
||||
item.status_label || "-"
|
||||
}}</el-tag>
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
|
||||
<div class="section-title">审批流程</div>
|
||||
<div class="flow">
|
||||
<div
|
||||
v-for="(node, idx) in timeline"
|
||||
:key="`${node.node_key}-${idx}`"
|
||||
class="flow-item"
|
||||
>
|
||||
<div class="flow-rail">
|
||||
<div class="avatar" :class="{ end: node.node_type === 3 }">
|
||||
{{ avatarText(node) }}
|
||||
</div>
|
||||
<span class="badge" :class="badgeClass(node)">{{
|
||||
badgeIcon(node)
|
||||
}}</span>
|
||||
<span v-if="idx !== timeline.length - 1" class="line" />
|
||||
</div>
|
||||
<div class="flow-main">
|
||||
<div class="flow-head">
|
||||
<div class="flow-title">
|
||||
<div class="flow-name">{{ nodeTitle(node) }}</div>
|
||||
<div class="flow-sub">{{ nodeSub(node) }}</div>
|
||||
</div>
|
||||
<el-tag size="small" :type="tagType(node.status_tag)">{{
|
||||
node.status_label
|
||||
}}</el-tag>
|
||||
</div>
|
||||
<div v-if="nodeTime(node)" class="flow-time">
|
||||
{{ nodeTime(node) }}
|
||||
</div>
|
||||
<div v-if="node.comment" class="flow-comment">
|
||||
{{ node.comment }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<el-empty v-if="!timeline.length" description="暂无流程节点" />
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<div class="drawer-footer">
|
||||
<el-button v-if="canWithdraw" @click="handleWithdraw">撤回审批</el-button>
|
||||
<template v-if="canApprove">
|
||||
<el-button type="danger" :loading="submitting" @click="handleApprove(false)"
|
||||
>驳回</el-button
|
||||
>
|
||||
<el-button type="primary" :loading="submitting" @click="handleApprove(true)"
|
||||
>通过</el-button
|
||||
>
|
||||
</template>
|
||||
<el-button @click="visible = false">关闭</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-drawer>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, ref, watch } from "vue";
|
||||
import { ElMessage, ElMessageBox } from "element-plus";
|
||||
import {
|
||||
approveApprovalInstance,
|
||||
getApprovalInstanceDetail,
|
||||
withdrawApprovalInstance,
|
||||
} from "@/api/approval";
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: { type: Boolean, default: false },
|
||||
instanceId: { type: [Number, String], default: "" },
|
||||
});
|
||||
const emit = defineEmits(["update:modelValue", "changed"]);
|
||||
|
||||
const visible = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (val) => emit("update:modelValue", val),
|
||||
});
|
||||
|
||||
const loading = ref(false);
|
||||
const submitting = ref(false);
|
||||
const item = ref(null);
|
||||
const timeline = ref([]);
|
||||
const flowName = ref("");
|
||||
const canApprove = ref(false);
|
||||
const canWithdraw = ref(false);
|
||||
const currentTaskId = ref(0);
|
||||
|
||||
const responseData = (res) => res?.data?.data ?? res?.data ?? res ?? {};
|
||||
|
||||
watch(
|
||||
() => [props.modelValue, props.instanceId],
|
||||
([open, id]) => {
|
||||
if (open && id) loadDetail(id);
|
||||
},
|
||||
);
|
||||
|
||||
async function loadDetail(id) {
|
||||
loading.value = true;
|
||||
try {
|
||||
const data = responseData(await getApprovalInstanceDetail(id));
|
||||
item.value = data.item || null;
|
||||
timeline.value = data.timeline || [];
|
||||
flowName.value = data.flow_name || "";
|
||||
canApprove.value = !!data.can_approve;
|
||||
canWithdraw.value = !!data.can_withdraw;
|
||||
currentTaskId.value = data.current_task_id || 0;
|
||||
} catch (error) {
|
||||
ElMessage.error(error?.message || "加载审批详情失败");
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function handleApprove(pass) {
|
||||
let comment = "";
|
||||
try {
|
||||
const { value } = await ElMessageBox.prompt(
|
||||
pass ? "审批意见(可留空)" : "请填写驳回原因",
|
||||
pass ? "审批通过" : "审批驳回",
|
||||
{ inputType: "textarea", inputPlaceholder: "请输入", confirmButtonText: "确定", cancelButtonText: "取消" },
|
||||
);
|
||||
comment = value || "";
|
||||
} catch (error) {
|
||||
return;
|
||||
}
|
||||
submitting.value = true;
|
||||
try {
|
||||
await approveApprovalInstance({
|
||||
task_id: currentTaskId.value,
|
||||
pass,
|
||||
comment,
|
||||
});
|
||||
ElMessage.success(pass ? "已通过" : "已驳回");
|
||||
emit("changed");
|
||||
await loadDetail(props.instanceId);
|
||||
} catch (error) {
|
||||
ElMessage.error(error?.message || "操作失败");
|
||||
} finally {
|
||||
submitting.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function handleWithdraw() {
|
||||
try {
|
||||
await ElMessageBox.confirm("撤回后本次审批立即结束,确认撤回吗?", "撤回确认", {
|
||||
type: "warning",
|
||||
});
|
||||
} catch (error) {
|
||||
return;
|
||||
}
|
||||
submitting.value = true;
|
||||
try {
|
||||
await withdrawApprovalInstance(props.instanceId);
|
||||
ElMessage.success("已撤回");
|
||||
emit("changed");
|
||||
await loadDetail(props.instanceId);
|
||||
} catch (error) {
|
||||
ElMessage.error(error?.message || "撤回失败");
|
||||
} finally {
|
||||
submitting.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
/* ===== 时间线展示辅助(与移动端保持一致的语义) ===== */
|
||||
function avatarText(node) {
|
||||
const name = String(node.approver_name || "").trim();
|
||||
return name ? name.slice(0, 1) : "系";
|
||||
}
|
||||
|
||||
function badgeClass(node) {
|
||||
if (node.is_current) return "badge-running";
|
||||
if (node.status === 1) return "badge-pass";
|
||||
if (node.status === 2) return "badge-reject";
|
||||
return "badge-skip";
|
||||
}
|
||||
|
||||
function badgeIcon(node) {
|
||||
if (node.is_current) return "·";
|
||||
if (node.status === 1) return "✓";
|
||||
if (node.status === 2) return "✕";
|
||||
return "—";
|
||||
}
|
||||
|
||||
function nodeTitle(node) {
|
||||
if (node.node_type === 1) return `${node.approver_name || "发起人"}(申请人)`;
|
||||
if (node.node_type === 3) return "流程结束";
|
||||
return node.approver_name || node.node_name || "审批节点";
|
||||
}
|
||||
|
||||
function nodeSub(node) {
|
||||
if (node.node_type === 1) return "提交申请";
|
||||
if (node.node_type === 3) return node.status_label || "";
|
||||
return node.approver_sub || node.node_name || "审批";
|
||||
}
|
||||
|
||||
function nodeTime(node) {
|
||||
const value = node.finish_time || node.create_time || "";
|
||||
return value ? String(value).slice(0, 16) : "";
|
||||
}
|
||||
|
||||
/** uview 的 tag 类型映射为 element-plus 的 tag 类型 */
|
||||
function tagType(type) {
|
||||
if (type === "error") return "danger";
|
||||
return type || "info";
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.section-title {
|
||||
position: relative;
|
||||
margin: 18px 0 12px;
|
||||
padding-left: 10px;
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
|
||||
&::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: 3px;
|
||||
height: 14px;
|
||||
border-radius: 2px;
|
||||
background: #2563eb;
|
||||
}
|
||||
}
|
||||
|
||||
.flow {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.flow-item {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.flow-rail {
|
||||
width: 40px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
position: relative;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 8px;
|
||||
background: linear-gradient(135deg, #60a5fa, #2563eb);
|
||||
color: #fff;
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 2;
|
||||
|
||||
&.end {
|
||||
background: linear-gradient(135deg, #cbd5e1, #94a3b8);
|
||||
}
|
||||
}
|
||||
|
||||
.badge {
|
||||
position: absolute;
|
||||
top: 26px;
|
||||
left: 26px;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border-radius: 8px;
|
||||
border: 2px solid #fff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 10px;
|
||||
line-height: 1;
|
||||
color: #fff;
|
||||
z-index: 3;
|
||||
|
||||
&.badge-pass {
|
||||
background: #16a34a;
|
||||
}
|
||||
&.badge-running {
|
||||
background: #f59e0b;
|
||||
}
|
||||
&.badge-reject {
|
||||
background: #ef4444;
|
||||
}
|
||||
&.badge-skip {
|
||||
background: #94a3b8;
|
||||
}
|
||||
}
|
||||
|
||||
.line {
|
||||
flex: 1;
|
||||
width: 0;
|
||||
margin-top: 6px;
|
||||
border-left: 1px dashed #cbd5e1;
|
||||
}
|
||||
|
||||
.flow-main {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
padding-bottom: 20px;
|
||||
}
|
||||
|
||||
.flow-head {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.flow-title {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.flow-name {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.flow-sub {
|
||||
margin-top: 2px;
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.flow-time {
|
||||
margin-top: 4px;
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.flow-comment {
|
||||
margin-top: 8px;
|
||||
padding: 8px 10px;
|
||||
background: #f1f5f9;
|
||||
border-radius: 6px;
|
||||
font-size: 13px;
|
||||
color: #606266;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.drawer-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
}
|
||||
</style>
|
||||
@@ -1,207 +1,147 @@
|
||||
<template>
|
||||
<div class="approval-center-page">
|
||||
<div class="approval-page">
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h2>审批列表</h2>
|
||||
<p>查看我参与的全部审批记录</p>
|
||||
<p>处理待我审批的单据,并查看已处理记录</p>
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
<el-radio-group v-model="filters.handled" @change="handleScopeChange">
|
||||
<el-radio-button :value="0">待我审批</el-radio-button>
|
||||
<el-radio-button :value="1">我已处理</el-radio-button>
|
||||
</el-radio-group>
|
||||
<el-button :icon="Refresh" @click="loadList">刷新</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="filter-card">
|
||||
<el-form :inline="true" :model="filters" @submit.prevent>
|
||||
<el-form-item label="单据类型">
|
||||
<el-select
|
||||
v-model="filters.biz_type"
|
||||
clearable
|
||||
placeholder="全部类型"
|
||||
style="width: 160px"
|
||||
@change="loadList"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in bizTypes"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" :icon="Search" @click="loadList">查询</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</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="table-scroll">
|
||||
<el-table v-loading="loading" :data="list" stripe>
|
||||
<el-table-column label="单据标题" min-width="200" show-overflow-tooltip>
|
||||
<template #default="{ row }">{{ row.instance?.biz_title || "-" }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="单据类型" width="120">
|
||||
<template #default="{ row }">{{ row.instance?.biz_type || "-" }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="发起人" width="120">
|
||||
<template #default="{ row }">{{ row.instance?.submit_user_name || "-" }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="node_name" label="审批节点" width="160" show-overflow-tooltip>
|
||||
<template #default="{ row }">{{ row.node_name || row.node_key || "-" }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="单据状态" width="110">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="tagType(row.instance?.status_tag)">{{
|
||||
row.instance?.status_label || "-"
|
||||
}}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="任务状态" width="110">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="tagType(row.status_tag)">{{ row.status_label || "-" }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="create_time" label="到达时间" width="170">
|
||||
<template #default="{ row }">{{ row.create_time || "-" }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" fixed="right" width="180">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" @click="openDetail(row.instance_id)">详情</el-button>
|
||||
<el-button
|
||||
v-if="row.status === 0"
|
||||
link
|
||||
type="primary"
|
||||
@click="openDetail(row.instance_id)"
|
||||
>去处理</el-button
|
||||
>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<template #empty><el-empty :description="emptyText" /></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'"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
@update:current-page="pagination.page = $event"
|
||||
@update:page-size="pagination.pageSize = $event"
|
||||
@current-change="loadCurrent"
|
||||
@size-change="loadCurrent"
|
||||
@current-change="loadList"
|
||||
@size-change="loadList"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ProgressTimeline v-model="progressVisible" :instance-id="progressInstanceId" />
|
||||
<DetailDrawer v-model="detailVisible" :instance-id="detailId" @changed="loadList" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { onMounted, reactive, ref } from "vue";
|
||||
import { computed, onMounted, reactive, ref } from "vue";
|
||||
import { ElMessage } from "element-plus";
|
||||
import ProgressTimeline from "./components/ProgressTimeline.vue";
|
||||
import { getCenterInstanceList, getCenterTaskList } from "@/api/approval";
|
||||
import { Refresh, Search } from "@element-plus/icons-vue";
|
||||
import DetailDrawer from "./components/DetailDrawer.vue";
|
||||
import { getApprovalTodo } from "@/api/approval";
|
||||
|
||||
const activeTab = ref("done");
|
||||
const taskList = ref([]);
|
||||
const instanceList = ref([]);
|
||||
const instanceStatus = ref(undefined);
|
||||
const loading = ref(false);
|
||||
const bizTypes = [
|
||||
{ value: "reimburse", label: "报销单" },
|
||||
{ value: "oa", label: "OA 通用审批" },
|
||||
];
|
||||
|
||||
const filters = reactive({ handled: 0, biz_type: "" });
|
||||
const pagination = reactive({ page: 1, pageSize: 10, total: 0 });
|
||||
const progressVisible = ref(false);
|
||||
const progressInstanceId = ref(0);
|
||||
const list = ref([]);
|
||||
const loading = ref(false);
|
||||
const detailVisible = ref(false);
|
||||
const detailId = ref("");
|
||||
|
||||
const isMobile = ref(false);
|
||||
const checkMobile = () => {
|
||||
isMobile.value = window.innerWidth <= 768;
|
||||
};
|
||||
const emptyText = computed(() =>
|
||||
filters.handled === 1 ? "暂无已处理的审批" : "暂无待我审批的单据",
|
||||
);
|
||||
|
||||
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) => {
|
||||
const loadList = async () => {
|
||||
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,
|
||||
await getApprovalTodo({
|
||||
handled: filters.handled,
|
||||
biz_type: filters.biz_type || "",
|
||||
page: pagination.page,
|
||||
pageSize: pagination.pageSize,
|
||||
}),
|
||||
);
|
||||
instanceList.value = data.list || [];
|
||||
// 前端二次过滤:类型下拉切换后即时生效
|
||||
const rows = data.list || [];
|
||||
list.value = filters.biz_type
|
||||
? rows.filter((row) => row.instance?.biz_type === filters.biz_type)
|
||||
: rows;
|
||||
pagination.total = Number(data.total || 0);
|
||||
} catch (error) {
|
||||
ElMessage.error(error?.message || "加载审批列表失败");
|
||||
@@ -210,103 +150,79 @@ const loadInstances = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
const loadCurrent = () => {
|
||||
if (activeTab.value === "submitted") loadInstances();
|
||||
else loadTasks(activeTab.value);
|
||||
};
|
||||
|
||||
const handleTabChange = () => {
|
||||
const handleScopeChange = () => {
|
||||
pagination.page = 1;
|
||||
pagination.total = 0;
|
||||
taskList.value = [];
|
||||
instanceList.value = [];
|
||||
loadCurrent();
|
||||
loadList();
|
||||
};
|
||||
|
||||
const openProgress = (row) => {
|
||||
progressInstanceId.value = row.instance_id || row.id;
|
||||
progressVisible.value = true;
|
||||
const openDetail = (id) => {
|
||||
detailId.value = id;
|
||||
detailVisible.value = true;
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
checkMobile();
|
||||
window.addEventListener("resize", checkMobile);
|
||||
loadCurrent();
|
||||
});
|
||||
function tagType(type) {
|
||||
if (type === "error") return "danger";
|
||||
return type || "info";
|
||||
}
|
||||
|
||||
onMounted(loadList);
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.approval-center-page {
|
||||
padding: 24px;
|
||||
.approval-page {
|
||||
background: #f5f7fa;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
|
||||
h2 {
|
||||
margin: 0 0 4px;
|
||||
font-size: 20px;
|
||||
color: var(--el-text-color-primary);
|
||||
margin: 0 0 8px;
|
||||
color: #303133;
|
||||
font-size: 22px;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
color: #909399;
|
||||
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 {
|
||||
.header-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-bottom: 12px;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.status-select {
|
||||
width: 160px;
|
||||
.filter-card,
|
||||
.table-card {
|
||||
background: #fff;
|
||||
border-radius: 6px;
|
||||
padding: 18px;
|
||||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.filter-card {
|
||||
margin-bottom: 16px;
|
||||
|
||||
:deep(.el-form-item) {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.table-scroll {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
margin-top: 16px;
|
||||
}
|
||||
</style>
|
||||
@@ -1,55 +1,86 @@
|
||||
<template>
|
||||
<div class="approval-center-page">
|
||||
<div class="approval-page">
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h2>待我审批</h2>
|
||||
<p>处理与我相关的 OA 审批事项</p>
|
||||
<h2>我的发起</h2>
|
||||
<p>查看自己发起的审批单据及当前进度</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>
|
||||
<el-button :icon="Refresh" @click="loadList">刷新</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="filter-card">
|
||||
<el-form :inline="true" :model="filters" @submit.prevent>
|
||||
<el-form-item label="关键词">
|
||||
<el-input
|
||||
v-model="filters.keyword"
|
||||
clearable
|
||||
placeholder="搜索单据标题"
|
||||
@keyup.enter="loadList"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="状态">
|
||||
<el-select
|
||||
v-model="filters.status"
|
||||
clearable
|
||||
placeholder="全部状态"
|
||||
class="status-select"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in statusOptions"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" :icon="Search" @click="loadList">查询</el-button>
|
||||
<el-button :icon="Refresh" @click="resetFilters">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</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">
|
||||
<el-table-column prop="biz_title" label="单据标题" min-width="200" show-overflow-tooltip>
|
||||
<template #default="{ row }">{{ row.biz_title || "-" }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="biz_type" label="单据类型" width="120">
|
||||
<template #default="{ row }">{{ row.biz_type || "-" }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="flow_name" label="审批流程" width="160" show-overflow-tooltip>
|
||||
<template #default="{ row }">{{ row.flow_name || "-" }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="status" label="状态" width="100">
|
||||
<template #default="{ row }">
|
||||
<el-tag size="small" effect="plain">{{ bizTypeLabel(row.biz_type) }}</el-tag>
|
||||
<el-tag :type="tagType(row.status_tag)">{{ row.status_label || "-" }}</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 prop="current_node" label="当前节点" width="160" show-overflow-tooltip>
|
||||
<template #default="{ row }">{{ row.current_node || "-" }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="submit_time" label="发起时间" width="150">
|
||||
<template #default="{ row }">{{ row.submit_time || "-" }}</template>
|
||||
<el-table-column prop="submit_time" label="提交时间" width="170">
|
||||
<template #default="{ row }">{{ row.submit_time || row.create_time || "-" }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="node_name" label="审批节点" width="140">
|
||||
<template #default="{ row }">{{ row.node_name || "-" }}</template>
|
||||
<el-table-column prop="finish_time" label="完成时间" width="170">
|
||||
<template #default="{ row }">{{ row.finish_time || "-" }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="deadline" label="处理时限" width="150">
|
||||
<el-table-column label="操作" fixed="right" width="140">
|
||||
<template #default="{ row }">
|
||||
<span :class="{ overdue: isOverdue(row.deadline) }">{{
|
||||
row.deadline || "-"
|
||||
}}</span>
|
||||
<el-button link type="primary" @click="openDetail(row.id)">详情</el-button>
|
||||
<el-button
|
||||
v-if="row.status === 1"
|
||||
link
|
||||
type="danger"
|
||||
@click="handleWithdraw(row)"
|
||||
>撤回</el-button
|
||||
>
|
||||
</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>
|
||||
<template #empty><el-empty description="暂无发起的审批" /></template>
|
||||
</el-table>
|
||||
</div>
|
||||
<div class="pagination">
|
||||
@@ -57,7 +88,7 @@
|
||||
:current-page="pagination.page"
|
||||
:page-size="pagination.pageSize"
|
||||
:total="pagination.total"
|
||||
:layout="isMobile ? 'prev, pager, next' : 'total, sizes, prev, pager, next, jumper'"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
@update:current-page="pagination.page = $event"
|
||||
@update:page-size="pagination.pageSize = $event"
|
||||
@current-change="loadList"
|
||||
@@ -66,165 +97,140 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ProgressTimeline v-model="progressVisible" :instance-id="progressInstanceId" />
|
||||
<DetailDrawer v-model="detailVisible" :instance-id="detailId" @changed="loadList" />
|
||||
</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";
|
||||
import { Refresh, Search } from "@element-plus/icons-vue";
|
||||
import DetailDrawer from "./components/DetailDrawer.vue";
|
||||
import { getMyApprovalInstances, withdrawApprovalInstance } from "@/api/approval";
|
||||
|
||||
const statusOptions = [
|
||||
{ value: 1, label: "审批中" },
|
||||
{ value: 2, label: "已通过" },
|
||||
{ value: 3, label: "已驳回" },
|
||||
{ value: 4, label: "已撤回" },
|
||||
{ value: 5, label: "已取消" },
|
||||
];
|
||||
|
||||
const filters = reactive({ keyword: "", status: undefined });
|
||||
const pagination = reactive({ page: 1, pageSize: 10, total: 0 });
|
||||
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 detailVisible = ref(false);
|
||||
const detailId = ref("");
|
||||
|
||||
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 }),
|
||||
await getMyApprovalInstances({
|
||||
...filters,
|
||||
status: filters.status ?? "",
|
||||
page: pagination.page,
|
||||
pageSize: pagination.pageSize,
|
||||
}),
|
||||
);
|
||||
list.value = data.list || [];
|
||||
pagination.total = Number(data.total || 0);
|
||||
} catch (error) {
|
||||
ElMessage.error(error?.message || "加载待办列表失败");
|
||||
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 || "操作失败");
|
||||
}
|
||||
};
|
||||
const resetFilters = () => {
|
||||
filters.keyword = "";
|
||||
filters.status = undefined;
|
||||
pagination.page = 1;
|
||||
loadList();
|
||||
};
|
||||
|
||||
if (pass) {
|
||||
ElMessageBox.confirm(`确认通过「${row.biz_title}」的「${row.node_name}」节点吗?`, "通过审批", {
|
||||
type: "success",
|
||||
confirmButtonText: "通过",
|
||||
cancelButtonText: "取消",
|
||||
}).then(() => doAct(""));
|
||||
} else {
|
||||
ElMessageBox.prompt("请输入驳回原因", "驳回", {
|
||||
const openDetail = (id) => {
|
||||
detailId.value = id;
|
||||
detailVisible.value = true;
|
||||
};
|
||||
|
||||
const handleWithdraw = async (row) => {
|
||||
try {
|
||||
await ElMessageBox.confirm(`确认撤回「${row.biz_title || "该单据"}」的审批吗?`, "撤回确认", {
|
||||
type: "warning",
|
||||
inputPlaceholder: "驳回原因(选填)",
|
||||
confirmButtonText: "驳回",
|
||||
cancelButtonText: "取消",
|
||||
}).then(({ value }) => doAct((value || "").trim()));
|
||||
});
|
||||
await withdrawApprovalInstance(row.id);
|
||||
ElMessage.success("已撤回");
|
||||
loadList();
|
||||
} catch (error) {
|
||||
if (error !== "cancel" && error !== "close")
|
||||
ElMessage.error(error?.message || "撤回失败");
|
||||
}
|
||||
};
|
||||
|
||||
const openProgress = (row) => {
|
||||
progressInstanceId.value = row.instance_id;
|
||||
progressVisible.value = true;
|
||||
};
|
||||
function tagType(type) {
|
||||
if (type === "error") return "danger";
|
||||
return type || "info";
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
checkMobile();
|
||||
window.addEventListener("resize", checkMobile);
|
||||
loadList();
|
||||
});
|
||||
onMounted(loadList);
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.approval-center-page {
|
||||
padding: 24px;
|
||||
.approval-page {
|
||||
background: #f5f7fa;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
|
||||
h2 {
|
||||
margin: 0 0 4px;
|
||||
font-size: 20px;
|
||||
color: var(--el-text-color-primary);
|
||||
margin: 0 0 8px;
|
||||
color: #303133;
|
||||
font-size: 22px;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
color: #909399;
|
||||
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;
|
||||
}
|
||||
|
||||
.filter-card,
|
||||
.table-card {
|
||||
padding: 16px;
|
||||
background: var(--el-bg-color);
|
||||
border-radius: 8px;
|
||||
background: #fff;
|
||||
border-radius: 6px;
|
||||
padding: 18px;
|
||||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.filter-card {
|
||||
margin-bottom: 16px;
|
||||
|
||||
:deep(.el-form-item) {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.status-select {
|
||||
width: 150px;
|
||||
}
|
||||
|
||||
.table-scroll {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
margin-top: 16px;
|
||||
}
|
||||
</style>
|
||||
@@ -1,297 +1,117 @@
|
||||
<template>
|
||||
<div class="approval-center-page">
|
||||
<div class="approval-settings-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>
|
||||
<h2>审批流设置</h2>
|
||||
<p>为本租户各功能模块编排审批流程:选择分类 → 新建流程 → 拖拽节点并保存</p>
|
||||
</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>
|
||||
<!-- 复用「系统管理 → 审批流配置」的编排界面,避免同一能力两份实现 -->
|
||||
<ApprovalFlowConfig />
|
||||
|
||||
<!-- 表单设计 -->
|
||||
<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 class="reference-card">
|
||||
<div class="section-title">流程编号引用</div>
|
||||
<p class="hint">
|
||||
保存后系统会为每个流程自动生成唯一的「审批流编号」,业务程序按编号调用:
|
||||
</p>
|
||||
<el-table v-loading="loading" :data="flows" size="small" stripe>
|
||||
<el-table-column prop="module_code" label="功能模块" width="120" />
|
||||
<el-table-column prop="flow_name" label="流程名称" min-width="180" show-overflow-tooltip />
|
||||
<el-table-column prop="flow_code" label="审批流编号" width="160" />
|
||||
<el-table-column label="状态" width="100">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.status === 1 ? 'success' : 'danger'">{{
|
||||
row.status === 1 ? "启用" : "停用"
|
||||
}}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<template #empty><el-empty description="暂无审批流,请先在上方新建" /></template>
|
||||
</el-table>
|
||||
</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";
|
||||
import { ElMessage } from "element-plus";
|
||||
import ApprovalFlowConfig from "@/views/system/approval/index.vue";
|
||||
import { getApprovalFlowList } 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 loading = ref(false);
|
||||
|
||||
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;
|
||||
loading.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 : [],
|
||||
}));
|
||||
flows.value = Array.isArray(data) ? data : data.list || [];
|
||||
} catch (error) {
|
||||
ElMessage.error(error?.message || "加载流程列表失败");
|
||||
ElMessage.error(error?.message || "加载审批流失败");
|
||||
} finally {
|
||||
flowsLoading.value = false;
|
||||
loading.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();
|
||||
});
|
||||
onMounted(loadFlows);
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.approval-center-page {
|
||||
padding: 24px;
|
||||
.approval-settings-page {
|
||||
background: #f5f7fa;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.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);
|
||||
margin: 0 0 8px;
|
||||
color: #303133;
|
||||
font-size: 22px;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
color: #909399;
|
||||
font-size: 13px;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
}
|
||||
|
||||
.table-card {
|
||||
padding: 8px 16px 16px;
|
||||
background: var(--el-bg-color);
|
||||
border-radius: 8px;
|
||||
.reference-card {
|
||||
margin-top: 16px;
|
||||
background: #fff;
|
||||
border-radius: 6px;
|
||||
padding: 18px;
|
||||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.dept-select {
|
||||
width: 100%;
|
||||
max-width: 320px;
|
||||
.section-title {
|
||||
position: relative;
|
||||
margin-bottom: 12px;
|
||||
padding-left: 10px;
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
|
||||
&::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: 3px;
|
||||
height: 14px;
|
||||
border-radius: 2px;
|
||||
background: #2563eb;
|
||||
}
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
.hint {
|
||||
margin: 0 0 12px;
|
||||
font-size: 13px;
|
||||
color: #909399;
|
||||
}
|
||||
</style>
|
||||
@@ -242,33 +242,16 @@
|
||||
@saved="handleItemsSaved"
|
||||
/>
|
||||
|
||||
<el-dialog
|
||||
v-model="manualConfirmVisible"
|
||||
title="手动确认"
|
||||
width="420px"
|
||||
class="manual-confirm-dialog"
|
||||
append-to-body
|
||||
destroy-on-close
|
||||
>
|
||||
<p class="manual-tip">选择要变更的单据状态,保存后立即生效。</p>
|
||||
<el-radio-group v-model="manualStatus" class="manual-status-group">
|
||||
<el-radio
|
||||
v-for="item in statusOptions"
|
||||
:key="item.value"
|
||||
:value="item.value"
|
||||
>
|
||||
{{ item.label }}
|
||||
</el-radio>
|
||||
</el-radio-group>
|
||||
<template #footer>
|
||||
<el-button @click="manualConfirmVisible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="manualSaving" @click="saveManualStatus">确认</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<template #footer>
|
||||
<div class="detail-toolbar">
|
||||
<el-button @click="openManualConfirm">手动确认</el-button>
|
||||
<el-button
|
||||
v-if="isApproved"
|
||||
type="success"
|
||||
:loading="manualSaving"
|
||||
@click="confirmPaid"
|
||||
>
|
||||
确认到账
|
||||
</el-button>
|
||||
<div class="toolbar-right">
|
||||
<el-button
|
||||
v-if="isDraft"
|
||||
@@ -327,6 +310,8 @@ const visible = computed({
|
||||
set: value => emit('update:modelValue', value)
|
||||
})
|
||||
const isDraft = computed(() => props.detail?.status === 0)
|
||||
// 审批通过(等待报销人确认款项到账)
|
||||
const isApproved = computed(() => props.detail?.status === 2)
|
||||
|
||||
const detailApplicantName = computed(() => {
|
||||
const name = props.detail?.user_name
|
||||
@@ -341,9 +326,7 @@ const detailApplicantName = computed(() => {
|
||||
return '-'
|
||||
})
|
||||
const submitting = ref(false)
|
||||
const manualConfirmVisible = ref(false)
|
||||
const manualSaving = ref(false)
|
||||
const manualStatus = ref(0)
|
||||
const itemVisible = ref(false)
|
||||
const batchUploading = ref(false)
|
||||
const invoiceFileInputRef = ref(null)
|
||||
@@ -712,23 +695,25 @@ const handleSubmit = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
const resolveManualStatus = status => (status >= 0 && status <= 5 ? status : 0)
|
||||
|
||||
const openManualConfirm = () => {
|
||||
manualStatus.value = resolveManualStatus(props.detail?.status ?? 0)
|
||||
manualConfirmVisible.value = true
|
||||
}
|
||||
|
||||
const saveManualStatus = async () => {
|
||||
// 审批通过后由报销人本人确认款项到账,单据置为「已打款」
|
||||
const confirmPaid = async () => {
|
||||
if (!props.detail?.id) return
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`确认已收到 ¥${Number(props.detail.total_amount || 0).toFixed(2)} 的报销款吗?确认后单据状态变为「已打款」。`,
|
||||
'确认到账',
|
||||
{ type: 'warning', confirmButtonText: '确认已到账', cancelButtonText: '暂未到账' }
|
||||
)
|
||||
} catch (error) {
|
||||
return
|
||||
}
|
||||
manualSaving.value = true
|
||||
try {
|
||||
await updateReimbursementStatus(props.detail.id, { status: manualStatus.value })
|
||||
ElMessage.success('状态已更新')
|
||||
manualConfirmVisible.value = false
|
||||
await updateReimbursementStatus(props.detail.id, { status: 5 })
|
||||
ElMessage.success('已确认到账')
|
||||
emit('changed', props.detail.id)
|
||||
} catch (error) {
|
||||
ElMessage.error(error?.message || '更新状态失败')
|
||||
ElMessage.error(error?.message || '确认到账失败')
|
||||
} finally {
|
||||
manualSaving.value = false
|
||||
}
|
||||
@@ -1066,19 +1051,6 @@ const money = value => Number(value || 0).toFixed(2)
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.manual-tip {
|
||||
margin: 0 0 16px;
|
||||
color: #909399;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.manual-status-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.amount-compare-card {
|
||||
margin-bottom: 16px;
|
||||
padding: 14px 16px;
|
||||
|
||||
@@ -92,6 +92,13 @@
|
||||
<el-button link type="primary" @click="openDetail(row.id)"
|
||||
>详情</el-button
|
||||
>
|
||||
<el-button
|
||||
v-if="row.status === 2"
|
||||
link
|
||||
type="success"
|
||||
@click="handleConfirmPaid(row)"
|
||||
>确认到账</el-button
|
||||
>
|
||||
<el-button
|
||||
link
|
||||
type="primary"
|
||||
@@ -181,6 +188,7 @@ import {
|
||||
getReimbursementRecords,
|
||||
submitReimbursement,
|
||||
updateReimbursement,
|
||||
updateReimbursementStatus,
|
||||
} from "@/api/reimburse";
|
||||
|
||||
const statusOptions = [
|
||||
@@ -304,6 +312,23 @@ const handleSubmit = async (row) => {
|
||||
}
|
||||
};
|
||||
|
||||
// 审批通过后由报销人本人确认款项到账,确认后状态变为「已打款」
|
||||
const handleConfirmPaid = async (row) => {
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`确认已收到 ¥${money(row.total_amount)} 的报销款吗?确认后单据状态变为「已打款」。`,
|
||||
"确认到账",
|
||||
{ type: "warning", confirmButtonText: "确认已到账", cancelButtonText: "暂未到账" }
|
||||
);
|
||||
await updateReimbursementStatus(row.id, { status: 5 });
|
||||
ElMessage.success("已确认到账");
|
||||
loadList();
|
||||
} catch (error) {
|
||||
if (error !== "cancel" && error !== "close")
|
||||
ElMessage.error(error?.message || "确认到账失败");
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = async (row) => {
|
||||
try {
|
||||
await ElMessageBox.confirm("确认删除这条报销记录吗?", "删除确认", {
|
||||
|
||||
Reference in new issue
Block a user