Files
qhmes/jeecgboot-vue3/src/components/ApprovalLaunch/index.vue
2026-06-10 16:57:07 +08:00

295 lines
9.1 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!--
全局发起审批悬浮按钮
钉钉审批按钮一致当前页存在 mes_xsl_ding_tpl_bind 绑定且钉钉模板启用时显示
弹窗内可选该页业务表下已发布的 MES 审批流
支持两种发起方式
1列表多选联动在列表勾选数据后点击弹窗自动带入选中单据并可批量发起
2手动选择未勾选时在弹窗内搜索选择单条单据发起
@author GHT
@date 2026-05-29 forQH-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, reactive, ref, watch } 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 { getBindingByRoute } from '/@/views/xslmes/dingtalk/dingTplBind/dingTplBind.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 binding = ref<any>(null);
const flowList = ref<any[]>([]);
const recordList = ref<any[]>([]);
// 打开弹窗时快照的列表勾选行(批量模式数据源)
const batchRows = ref<any[]>([]);
// 悬浮位置(右下角)
const floatStyle = reactive({ right: '24px', bottom: '120px' });
// 与钉钉审批按钮一致:按 mes_xsl_ding_tpl_bind + 路由解析是否显示
const show = computed(() => !!binding.value);
const matchedFlows = computed(() => flowList.value);
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,
}))
);
watch(
() => currentRoute.value?.path,
async (path) => {
binding.value = null;
flowList.value = [];
if (!path || path === '/' || path.startsWith('/login')) {
return;
}
try {
const bind = await getBindingByRoute(path);
binding.value = bind || null;
if (binding.value) {
flowList.value = (await getPublishedFlows(path)) || [];
}
} catch {
binding.value = null;
flowList.value = [];
}
},
{ immediate: true },
);
function resetSelection() {
flowId.value = undefined;
bizDataId.value = undefined;
bizTitle.value = '';
recordList.value = [];
batchRows.value = [];
}
async function openModal() {
if (!matchedFlows.value.length) {
createMessage.warning('当前页面暂无已发布的 MES 审批流,请先在审批流设计中发布');
return;
}
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>