<!--OA模块:审批模板(系统常用 + 自定义多节点流程)-->
|
<template>
|
<div class="app-container approve-template-page">
|
<el-tabs v-model="activeTab" class="template-tabs">
|
<el-tab-pane label="系统常用审批" name="builtin">
|
<el-alert type="info" show-icon :closable="false" class="mb16">
|
<template #title>系统预置模板</template>
|
<template #default>
|
以下为 OA 模块内置的常用审批类型,填报字段与默认审批方式由系统维护;提交审批时可直接选用。
|
</template>
|
</el-alert>
|
<div class="builtin-grid">
|
<div v-for="item in builtinTemplates" :key="item.key" class="builtin-card">
|
<span class="builtin-label">{{ item.label }}</span>
|
<p class="builtin-summary">{{ item.summary }}</p>
|
<div class="builtin-meta">
|
<el-tag size="small" effect="plain">{{ item.fieldCount }} 个填报项</el-tag>
|
<el-tag size="small" type="warning" effect="plain">
|
默认{{ item.defaultMode === "or_sign" ? "或签" : "与签" }}
|
</el-tag>
|
<el-tag size="small" type="info" effect="plain">只读</el-tag>
|
</div>
|
</div>
|
</div>
|
</el-tab-pane>
|
|
<el-tab-pane label="自定义审批模板" name="custom">
|
<div class="search_form mb20">
|
<div class="search_fields">
|
<span class="search_title">模板名称:</span>
|
<el-input
|
v-model="searchForm.keyword"
|
style="width: 220px"
|
placeholder="搜索名称或说明"
|
clearable
|
:prefix-icon="Search"
|
@keyup.enter="handleQuery"
|
/>
|
<el-checkbox v-model="searchForm.enabledOnly" class="ml12" @change="handleQuery">
|
仅显示启用
|
</el-checkbox>
|
<el-button type="primary" :icon="Search" class="ml10" @click="handleQuery">搜索</el-button>
|
<el-button :icon="RefreshRight" @click="resetSearch">重置</el-button>
|
</div>
|
<div class="search_actions">
|
<el-button type="primary" :icon="Plus" @click="openFormDialog('add')">新建模板</el-button>
|
</div>
|
</div>
|
|
<div class="table_list">
|
<PIMTable
|
rowKey="id"
|
:column="tableColumn"
|
:tableData="tableData"
|
:page="page"
|
:isSelection="false"
|
:tableLoading="tableLoading"
|
:total="page.total"
|
@pagination="pagination"
|
/>
|
</div>
|
</el-tab-pane>
|
</el-tabs>
|
|
<!-- 新建 / 编辑 -->
|
<el-dialog
|
v-model="formDialog.visible"
|
:title="formDialog.title"
|
width="960px"
|
append-to-body
|
destroy-on-close
|
class="template-form-dialog"
|
@closed="formRef?.resetFields?.()"
|
>
|
<el-form ref="formRef" :model="form" :rules="formRules" label-width="100px">
|
<el-row :gutter="20">
|
<el-col :span="12">
|
<el-form-item label="模板名称" prop="templateName">
|
<el-input v-model="form.templateName" placeholder="如:项目立项审批" maxlength="50" show-word-limit />
|
</el-form-item>
|
</el-col>
|
<el-col :span="12">
|
<el-form-item label="启用状态">
|
<el-switch v-model="form.enabled" active-text="启用" inactive-text="停用" />
|
</el-form-item>
|
</el-col>
|
</el-row>
|
<el-form-item label="模板说明">
|
<el-input
|
v-model="form.description"
|
type="textarea"
|
:rows="2"
|
placeholder="简要说明该模板的适用场景"
|
maxlength="200"
|
show-word-limit
|
/>
|
</el-form-item>
|
<el-form-item label="审批流程" required>
|
<TemplateFlowEditor v-model="form.flowNodes" :user-options="flowUserOptions" />
|
<p class="flow-tip">
|
按顺序流转:可为每个节点添加多名审批人;会签需全部通过,或签任一人通过即可进入下一节点。
|
</p>
|
</el-form-item>
|
</el-form>
|
<template #footer>
|
<el-button type="primary" @click="onSubmitForm">保 存</el-button>
|
<el-button @click="formDialog.visible = false">取 消</el-button>
|
</template>
|
</el-dialog>
|
|
<!-- 详情 -->
|
<el-dialog v-model="detailDialog.visible" title="模板详情" width="880px" append-to-body destroy-on-close>
|
<el-descriptions :column="2" border>
|
<el-descriptions-item label="模板名称">{{ detailRow.templateName }}</el-descriptions-item>
|
<el-descriptions-item label="状态">
|
<el-tag :type="detailRow.enabled !== false ? 'success' : 'info'" size="small">
|
{{ detailRow.enabled !== false ? "启用" : "停用" }}
|
</el-tag>
|
</el-descriptions-item>
|
<el-descriptions-item label="说明" :span="2">{{ detailRow.description || "—" }}</el-descriptions-item>
|
<el-descriptions-item label="创建时间">{{ detailRow.createTime || "—" }}</el-descriptions-item>
|
<el-descriptions-item label="更新时间">{{ detailRow.updateTime || "—" }}</el-descriptions-item>
|
</el-descriptions>
|
<el-divider content-position="left">审批流程({{ detailRow.flowNodes?.length || 0 }} 个节点)</el-divider>
|
<div v-if="detailRow.flowNodes?.length" class="detail-flow">
|
<div v-for="(node, index) in detailRow.flowNodes" :key="index" class="detail-node">
|
<div class="detail-node-head">
|
<span class="detail-node-order">节点 {{ index + 1 }}</span>
|
<el-tag size="small" :type="node.signMode === 'or_sign' ? 'warning' : 'primary'">
|
{{ nodeSignModeLabel(node.signMode) }}
|
</el-tag>
|
</div>
|
<div class="detail-approvers">
|
<el-tag
|
v-for="a in node.approvers"
|
:key="String(a.approverId)"
|
class="detail-approver-tag"
|
effect="plain"
|
>
|
{{ a.approverName || "—" }}
|
</el-tag>
|
<span v-if="!node.approvers?.length" class="text-muted">未配置审批人</span>
|
</div>
|
<el-icon v-if="index < detailRow.flowNodes.length - 1" class="detail-arrow"><ArrowRight /></el-icon>
|
</div>
|
</div>
|
<el-empty v-else description="暂无流程节点" :image-size="60" />
|
<template #footer>
|
<el-button @click="detailDialog.visible = false">关 闭</el-button>
|
<el-button type="primary" @click="editFromDetail">编 辑</el-button>
|
</template>
|
</el-dialog>
|
</div>
|
</template>
|
|
<script setup>
|
import { ArrowRight, Document, Plus, RefreshRight } from "@element-plus/icons-vue";
|
import { ElMessage } from "element-plus";
|
import { onMounted, ref } from "vue";
|
import { userListNoPageByTenantId } from "@/api/system/user.js";
|
import TemplateFlowEditor from "./components/TemplateFlowEditor.vue";
|
import { useApproveTemplate } from "./useApproveTemplate.js";
|
|
const at = useApproveTemplate();
|
const {
|
Search,
|
activeTab,
|
builtinTemplates,
|
nodeSignModeLabel,
|
searchForm,
|
tableLoading,
|
page,
|
tableData,
|
tableColumn,
|
formDialog,
|
form,
|
formRef,
|
formRules,
|
detailDialog,
|
detailRow,
|
handleQuery,
|
resetSearch,
|
pagination,
|
openFormDialog,
|
openDetail,
|
submitForm,
|
} = at;
|
|
const flowUserOptions = ref([]);
|
|
function unwrapArray(payload) {
|
if (Array.isArray(payload)) return payload;
|
if (payload?.data && Array.isArray(payload.data)) return payload.data;
|
if (payload?.rows && Array.isArray(payload.rows)) return payload.rows;
|
return [];
|
}
|
|
function isActiveUser(u) {
|
if (u.delFlag === "2" || u.delFlag === 2) return false;
|
if (u.status == null) return true;
|
return String(u.status) === "0";
|
}
|
|
async function loadUsers() {
|
try {
|
const res = await userListNoPageByTenantId();
|
flowUserOptions.value = unwrapArray(res).filter(isActiveUser);
|
} catch {
|
flowUserOptions.value = [];
|
}
|
}
|
|
async function onSubmitForm() {
|
const ret = await submitForm();
|
if (ret?.message) {
|
ElMessage.warning(ret.message);
|
return;
|
}
|
if (ret?.ok) ElMessage.success("保存成功");
|
}
|
|
function editFromDetail() {
|
const row = detailRow.value;
|
detailDialog.visible = false;
|
openFormDialog("edit", row);
|
}
|
|
onMounted(() => {
|
loadUsers();
|
handleQuery();
|
});
|
</script>
|
|
<style scoped>
|
.mb20 {
|
margin-bottom: 20px;
|
}
|
.mb16 {
|
margin-bottom: 16px;
|
}
|
.ml10 {
|
margin-left: 10px;
|
}
|
.ml12 {
|
margin-left: 12px;
|
}
|
.page-header .header-title {
|
display: flex;
|
align-items: center;
|
gap: 8px;
|
font-size: 18px;
|
font-weight: 600;
|
color: var(--el-text-color-primary);
|
margin-bottom: 8px;
|
}
|
.title-icon {
|
font-size: 22px;
|
color: var(--el-color-primary);
|
}
|
.header-desc {
|
margin: 0;
|
font-size: 13px;
|
color: var(--el-text-color-secondary);
|
line-height: 1.6;
|
max-width: 920px;
|
}
|
.search_form {
|
display: flex;
|
flex-wrap: wrap;
|
align-items: center;
|
justify-content: space-between;
|
gap: 12px;
|
}
|
.search_fields {
|
display: flex;
|
flex-wrap: wrap;
|
align-items: center;
|
gap: 4px;
|
}
|
.search_actions {
|
display: flex;
|
gap: 8px;
|
}
|
.builtin-grid {
|
display: grid;
|
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
|
gap: 12px;
|
}
|
.builtin-card {
|
padding: 14px 16px;
|
border: 1px solid var(--el-border-color-lighter);
|
border-radius: var(--radius-md, 8px);
|
background: var(--el-fill-color-blank);
|
}
|
.builtin-label {
|
font-size: 15px;
|
font-weight: 600;
|
color: var(--el-text-color-primary);
|
}
|
.builtin-summary {
|
margin: 8px 0 10px;
|
font-size: 12px;
|
color: var(--el-text-color-secondary);
|
line-height: 1.5;
|
min-height: 36px;
|
}
|
.builtin-meta {
|
display: flex;
|
flex-wrap: wrap;
|
gap: 6px;
|
}
|
.flow-tip {
|
font-size: 12px;
|
color: var(--el-text-color-secondary);
|
margin: 8px 0 0;
|
line-height: 1.5;
|
}
|
.detail-flow {
|
display: flex;
|
flex-wrap: wrap;
|
align-items: flex-start;
|
gap: 8px;
|
}
|
.detail-node {
|
position: relative;
|
min-width: 180px;
|
max-width: 240px;
|
padding: 12px;
|
border: 1px solid var(--el-border-color-lighter);
|
border-radius: 8px;
|
background: var(--el-fill-color-lighter);
|
}
|
.detail-node-head {
|
display: flex;
|
align-items: center;
|
justify-content: space-between;
|
margin-bottom: 8px;
|
}
|
.detail-node-order {
|
font-weight: 600;
|
font-size: 13px;
|
}
|
.detail-approvers {
|
display: flex;
|
flex-wrap: wrap;
|
gap: 4px;
|
}
|
.detail-approver-tag {
|
margin: 0;
|
}
|
.detail-arrow {
|
position: absolute;
|
right: -20px;
|
top: 50%;
|
transform: translateY(-50%);
|
color: var(--el-text-color-placeholder);
|
}
|
.text-muted {
|
font-size: 12px;
|
color: var(--el-text-color-placeholder);
|
}
|
.template-form-dialog :deep(.el-dialog__body) {
|
padding-top: 8px;
|
}
|
</style>
|