新增MES审批流设计功能,包括审批流定义、审批实例管理及审批办理接口,支持可视化设计与业务单据联动,提升审批流程的灵活性与用户体验。
This commit is contained in:
113
jeecgboot-vue3/src/components/ApprovalDesign/index.vue
Normal file
113
jeecgboot-vue3/src/components/ApprovalDesign/index.vue
Normal file
@@ -0,0 +1,113 @@
|
||||
<!--
|
||||
全局「审批流程设计」悬浮按钮
|
||||
拥有 approval:flow:design 权限的用户,在任意功能页点击即可:
|
||||
1)后端按当前页路由反查绑定的业务表;
|
||||
2)解析该表字段,识别「校对/审核/审批/分发/抄送」等阶段字段(不存在不报错);
|
||||
3)进入可视化设计器,可点选识别到的阶段字段按顺序生成审批流程并保存发布。
|
||||
@author GHT
|
||||
@date 2026-05-29 for:【QH-MES审批流设计】全局审批流程设计悬浮按钮
|
||||
-->
|
||||
<template>
|
||||
<div v-if="show" class="approval-design-float" :style="floatStyle">
|
||||
<div class="approval-design-btn" :class="{ 'is-loading': loading }" title="审批流程设计" @click="openDesigner">
|
||||
<Icon :icon="loading ? 'ant-design:loading-outlined' : 'ant-design:partition-outlined'" :size="20" :spin="loading" />
|
||||
<span class="approval-design-text">流程设计</span>
|
||||
</div>
|
||||
<!-- 流程设计器(全屏) -->
|
||||
<FlowDesign @register="registerDesign" @success="onSuccess" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, reactive, ref } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { useMessage } from '/@/hooks/web/useMessage';
|
||||
import { usePermission } from '/@/hooks/web/usePermission';
|
||||
import { useModal } from '/@/components/Modal';
|
||||
import { getApprovalDesignContext } from '/@/views/approval/flow/approvalFlow.api';
|
||||
import FlowDesign from '/@/views/approval/flow/components/FlowDesign.vue';
|
||||
|
||||
defineOptions({ name: 'ApprovalDesignFloat' });
|
||||
|
||||
const { createMessage } = useMessage();
|
||||
const { currentRoute } = useRouter();
|
||||
const { hasPermission } = usePermission();
|
||||
const [registerDesign, { openModal: openDesign }] = useModal();
|
||||
|
||||
const loading = ref(false);
|
||||
// 悬浮位置(位于「发起审批」按钮上方)
|
||||
const floatStyle = reactive({ right: '24px', bottom: '190px' });
|
||||
|
||||
// 仅拥有设计权限的用户可见
|
||||
const show = computed(() => hasPermission('approval:flow:design'));
|
||||
|
||||
function normalizePath(p?: string) {
|
||||
return (p || '').trim().replace(/\/+$/, '');
|
||||
}
|
||||
|
||||
async function openDesigner() {
|
||||
if (loading.value) return;
|
||||
const path = normalizePath(currentRoute.value?.path);
|
||||
if (!path) return;
|
||||
try {
|
||||
loading.value = true;
|
||||
const ctx: any = await getApprovalDesignContext(path);
|
||||
if (!ctx || !ctx.bizTable || !ctx.flow) {
|
||||
createMessage.info('当前页面未能识别到可绑定的业务单据,无法设计审批流程');
|
||||
return;
|
||||
}
|
||||
openDesign(true, {
|
||||
record: ctx.flow,
|
||||
readonly: false,
|
||||
paletteStages: ctx.stages || [],
|
||||
});
|
||||
} catch (e: any) {
|
||||
createMessage.error(e?.message || '获取设计上下文失败');
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function onSuccess() {
|
||||
createMessage.success('审批流程已保存');
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.approval-design-float {
|
||||
position: fixed;
|
||||
z-index: 999;
|
||||
|
||||
.approval-design-btn {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 2px;
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(135deg, #15bca3, #0e9e88);
|
||||
color: #fff;
|
||||
cursor: pointer;
|
||||
box-shadow: 0 4px 14px rgba(21, 188, 163, 0.45);
|
||||
transition: transform 0.18s, box-shadow 0.18s;
|
||||
user-select: none;
|
||||
|
||||
.approval-design-text {
|
||||
font-size: 11px;
|
||||
line-height: 1;
|
||||
transform: scale(0.92);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
transform: scale(1.08);
|
||||
box-shadow: 0 6px 20px rgba(21, 188, 163, 0.6);
|
||||
}
|
||||
|
||||
&.is-loading {
|
||||
cursor: progress;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
283
jeecgboot-vue3/src/components/ApprovalLaunch/index.vue
Normal file
283
jeecgboot-vue3/src/components/ApprovalLaunch/index.vue
Normal file
@@ -0,0 +1,283 @@
|
||||
<!--
|
||||
全局「发起审批」悬浮按钮
|
||||
仅在「设计并发布了审批流、且能匹配到对应功能页路由」的页面显示。
|
||||
支持两种发起方式:
|
||||
1)列表多选联动:在列表勾选数据后点击,弹窗自动带入选中单据并可批量发起;
|
||||
2)手动选择:未勾选时,在弹窗内搜索选择单条单据发起。
|
||||
@author GHT
|
||||
@date 2026-05-29 for:【QH-MES审批流设计】发起审批运行时
|
||||
-->
|
||||
<template>
|
||||
<div v-if="show" class="approval-float" :style="floatStyle">
|
||||
<div class="approval-float-btn" title="发起审批" @click="openModal">
|
||||
<Icon icon="ant-design:audit-outlined" :size="20" />
|
||||
<span class="approval-float-text">发起审批</span>
|
||||
</div>
|
||||
|
||||
<a-modal v-model:open="visible" title="发起审批" :width="540" :confirmLoading="loading" okText="发起审批" @ok="handleLaunch">
|
||||
<a-form layout="vertical" style="margin-top: 8px">
|
||||
<a-form-item label="单据类型(审批流)" required>
|
||||
<a-select v-model:value="flowId" placeholder="请选择审批流" :options="flowOptions" @change="onFlowChange" allowClear />
|
||||
</a-form-item>
|
||||
|
||||
<!-- 批量模式:直接展示列表勾选的单据 -->
|
||||
<a-form-item v-if="isBatch" :label="`已选单据(共 ${batchItems.length} 条)`" required>
|
||||
<div class="approval-float-batch">
|
||||
<div v-for="it in batchItems" :key="it.bizDataId" class="approval-float-batch-item">
|
||||
<Icon icon="ant-design:file-text-outlined" :size="14" />
|
||||
<span class="approval-float-batch-title">{{ it.bizTitle }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</a-form-item>
|
||||
|
||||
<!-- 手动模式:搜索选择单条单据 -->
|
||||
<a-form-item v-else label="选择单据" required>
|
||||
<a-select
|
||||
v-model:value="bizDataId"
|
||||
show-search
|
||||
placeholder="请选择需要发起审批的单据"
|
||||
:filter-option="false"
|
||||
:options="recordOptions"
|
||||
:disabled="!flowId"
|
||||
@search="onSearch"
|
||||
@change="onRecordChange"
|
||||
>
|
||||
<template #notFoundContent>
|
||||
<a-spin v-if="recordLoading" size="small" />
|
||||
<span v-else>无单据数据</span>
|
||||
</template>
|
||||
</a-select>
|
||||
<div v-if="flowId && !recordOptions.length && !recordLoading" class="approval-float-tip">
|
||||
该单据暂无数据,或审批流未配置“单据标题字段”
|
||||
</div>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, onMounted, reactive, ref } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { debounce } from 'lodash-es';
|
||||
import { useMessage } from '/@/hooks/web/useMessage';
|
||||
import { getPublishedFlows, getBizRecords, launchApproval, launchApprovalBatch } from '/@/views/approval/flow/launch.api';
|
||||
import { useApprovalSelection } from './useApprovalSelection';
|
||||
|
||||
defineOptions({ name: 'ApprovalLaunchFloat' });
|
||||
|
||||
const { createMessage } = useMessage();
|
||||
const { currentRoute } = useRouter();
|
||||
const approvalSelection = useApprovalSelection();
|
||||
|
||||
const visible = ref(false);
|
||||
const loading = ref(false);
|
||||
const recordLoading = ref(false);
|
||||
|
||||
const flowId = ref<string>();
|
||||
const bizDataId = ref<string>();
|
||||
const bizTitle = ref<string>('');
|
||||
|
||||
const flowList = ref<any[]>([]);
|
||||
const recordList = ref<any[]>([]);
|
||||
// 打开弹窗时快照的列表勾选行(批量模式数据源)
|
||||
const batchRows = ref<any[]>([]);
|
||||
|
||||
// 悬浮位置(右下角)
|
||||
const floatStyle = reactive({ right: '24px', bottom: '120px' });
|
||||
|
||||
function normalizePath(p?: string) {
|
||||
return (p || '').trim().replace(/\/+$/, '');
|
||||
}
|
||||
|
||||
// 当前路由匹配到的已发布审批流
|
||||
const matchedFlows = computed(() => {
|
||||
const cur = normalizePath(currentRoute.value?.path);
|
||||
if (!cur) return [];
|
||||
return flowList.value.filter((f) => f.routePath && normalizePath(f.routePath) === cur);
|
||||
});
|
||||
|
||||
const show = computed(() => matchedFlows.value.length > 0);
|
||||
|
||||
const flowOptions = computed(() =>
|
||||
matchedFlows.value.map((f) => ({
|
||||
label: `${f.flowName}(${f.bizTableName || f.bizTable})`,
|
||||
value: f.id,
|
||||
}))
|
||||
);
|
||||
|
||||
// 当前选中的审批流对象
|
||||
const currentFlow = computed(() => matchedFlows.value.find((f) => f.id === flowId.value));
|
||||
|
||||
// 是否批量模式(列表有勾选)
|
||||
const isBatch = computed(() => batchRows.value.length > 0);
|
||||
|
||||
// 批量模式下的单据项(用审批流的标题字段取展示标题)
|
||||
const batchItems = computed(() => {
|
||||
const titleField = currentFlow.value?.titleField;
|
||||
return batchRows.value
|
||||
.filter((r) => r && r.id != null)
|
||||
.map((r) => ({
|
||||
bizDataId: String(r.id),
|
||||
bizTitle: titleField && r[titleField] != null ? String(r[titleField]) : String(r.id),
|
||||
}));
|
||||
});
|
||||
|
||||
const recordOptions = computed(() =>
|
||||
recordList.value.map((r) => ({
|
||||
label: r.title ?? r.id,
|
||||
value: r.id,
|
||||
}))
|
||||
);
|
||||
|
||||
onMounted(loadFlows);
|
||||
|
||||
async function loadFlows() {
|
||||
try {
|
||||
flowList.value = (await getPublishedFlows()) || [];
|
||||
} catch {
|
||||
flowList.value = [];
|
||||
}
|
||||
}
|
||||
|
||||
function resetSelection() {
|
||||
flowId.value = undefined;
|
||||
bizDataId.value = undefined;
|
||||
bizTitle.value = '';
|
||||
recordList.value = [];
|
||||
batchRows.value = [];
|
||||
}
|
||||
|
||||
async function openModal() {
|
||||
visible.value = true;
|
||||
resetSelection();
|
||||
// 读取当前列表页的勾选行
|
||||
batchRows.value = approvalSelection.getRowsByPath(currentRoute.value?.path || '');
|
||||
// 当前页只匹配到一个审批流时自动选中
|
||||
if (matchedFlows.value.length === 1) {
|
||||
flowId.value = matchedFlows.value[0].id;
|
||||
}
|
||||
// 手动模式且已确定审批流时预加载单据列表
|
||||
if (!isBatch.value && flowId.value) {
|
||||
await loadRecords();
|
||||
}
|
||||
}
|
||||
|
||||
async function loadRecords(keyword?: string) {
|
||||
if (!flowId.value) return;
|
||||
try {
|
||||
recordLoading.value = true;
|
||||
recordList.value = (await getBizRecords({ flowId: flowId.value, keyword })) || [];
|
||||
} finally {
|
||||
recordLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function onFlowChange() {
|
||||
bizDataId.value = undefined;
|
||||
bizTitle.value = '';
|
||||
recordList.value = [];
|
||||
if (!isBatch.value && flowId.value) loadRecords();
|
||||
}
|
||||
|
||||
const onSearch = debounce((val: string) => {
|
||||
loadRecords(val);
|
||||
}, 350);
|
||||
|
||||
function onRecordChange() {
|
||||
const hit = recordList.value.find((r) => r.id === bizDataId.value);
|
||||
bizTitle.value = hit ? hit.title ?? hit.id : '';
|
||||
}
|
||||
|
||||
async function handleLaunch() {
|
||||
if (!flowId.value) {
|
||||
createMessage.warning('请选择审批流');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
loading.value = true;
|
||||
if (isBatch.value) {
|
||||
await launchApprovalBatch({ flowId: flowId.value, items: batchItems.value });
|
||||
createMessage.success(`已发起 ${batchItems.value.length} 条审批!`);
|
||||
} else {
|
||||
if (!bizDataId.value) {
|
||||
createMessage.warning('请选择需要发起审批的单据');
|
||||
return;
|
||||
}
|
||||
await launchApproval({ flowId: flowId.value, bizDataId: bizDataId.value, bizTitle: bizTitle.value });
|
||||
createMessage.success('发起成功!');
|
||||
}
|
||||
visible.value = false;
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.approval-float {
|
||||
position: fixed;
|
||||
z-index: 999;
|
||||
|
||||
.approval-float-btn {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 2px;
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(135deg, #3296fa, #1668dc);
|
||||
color: #fff;
|
||||
cursor: pointer;
|
||||
box-shadow: 0 4px 14px rgba(50, 150, 250, 0.45);
|
||||
transition: transform 0.18s, box-shadow 0.18s;
|
||||
user-select: none;
|
||||
|
||||
.approval-float-text {
|
||||
font-size: 11px;
|
||||
line-height: 1;
|
||||
transform: scale(0.92);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
transform: scale(1.08);
|
||||
box-shadow: 0 6px 20px rgba(50, 150, 250, 0.6);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.approval-float-batch {
|
||||
max-height: 220px;
|
||||
overflow-y: auto;
|
||||
border: 1px solid #f0f0f0;
|
||||
border-radius: 6px;
|
||||
padding: 6px 8px;
|
||||
|
||||
.approval-float-batch-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 4px 2px;
|
||||
font-size: 13px;
|
||||
border-bottom: 1px dashed #f0f0f0;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.approval-float-batch-title {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.approval-float-tip {
|
||||
margin-top: 6px;
|
||||
font-size: 12px;
|
||||
color: #faad14;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,33 @@
|
||||
import { ref } from 'vue';
|
||||
|
||||
/**
|
||||
* 审批发起-列表选中上下文(全局单例)
|
||||
* 由 useListPage 自动把当前列表页的选中行同步进来,
|
||||
* 全局「发起审批」悬浮按钮发起时直接读取,实现"列表多选 -> 发起弹窗"联动。
|
||||
*
|
||||
* @author GHT
|
||||
* @date 2026-05-29 for:【QH-MES审批流设计】发起审批支持列表多选联动
|
||||
*/
|
||||
// 当前选中的行记录
|
||||
const rows = ref<any[]>([]);
|
||||
// 选中来源页面路由,用于校验与当前页是否一致,避免跨页串数据
|
||||
const sourcePath = ref<string>('');
|
||||
|
||||
export function useApprovalSelection() {
|
||||
function setSelection(list: any[], path: string) {
|
||||
rows.value = Array.isArray(list) ? [...list] : [];
|
||||
sourcePath.value = path || '';
|
||||
}
|
||||
|
||||
function clear() {
|
||||
rows.value = [];
|
||||
sourcePath.value = '';
|
||||
}
|
||||
|
||||
/** 获取与指定路由匹配的选中行(不匹配返回空) */
|
||||
function getRowsByPath(path: string): any[] {
|
||||
return sourcePath.value === path ? rows.value : [];
|
||||
}
|
||||
|
||||
return { rows, sourcePath, setSelection, clear, getRowsByPath };
|
||||
}
|
||||
Reference in New Issue
Block a user