2026-05-29 15:49:10 +08:00
|
|
|
|
<!--
|
|
|
|
|
|
全局「发起审批」悬浮按钮
|
2026-06-10 16:57:07 +08:00
|
|
|
|
与「钉钉审批」按钮一致:当前页存在 mes_xsl_ding_tpl_bind 绑定且钉钉模板启用时显示;
|
|
|
|
|
|
弹窗内可选该页业务表下已发布的 MES 审批流。
|
2026-05-29 15:49:10 +08:00
|
|
|
|
支持两种发起方式:
|
|
|
|
|
|
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>
|
2026-06-10 16:57:07 +08:00
|
|
|
|
import { computed, reactive, ref, watch } from 'vue';
|
2026-05-29 15:49:10 +08:00
|
|
|
|
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';
|
2026-06-10 16:57:07 +08:00
|
|
|
|
import { getBindingByRoute } from '/@/views/xslmes/dingtalk/dingTplBind/dingTplBind.api';
|
2026-05-29 15:49:10 +08:00
|
|
|
|
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>('');
|
|
|
|
|
|
|
2026-06-10 16:57:07 +08:00
|
|
|
|
const binding = ref<any>(null);
|
2026-05-29 15:49:10 +08:00
|
|
|
|
const flowList = ref<any[]>([]);
|
|
|
|
|
|
const recordList = ref<any[]>([]);
|
|
|
|
|
|
// 打开弹窗时快照的列表勾选行(批量模式数据源)
|
|
|
|
|
|
const batchRows = ref<any[]>([]);
|
|
|
|
|
|
|
|
|
|
|
|
// 悬浮位置(右下角)
|
|
|
|
|
|
const floatStyle = reactive({ right: '24px', bottom: '120px' });
|
|
|
|
|
|
|
2026-06-10 16:57:07 +08:00
|
|
|
|
// 与钉钉审批按钮一致:按 mes_xsl_ding_tpl_bind + 路由解析是否显示
|
|
|
|
|
|
const show = computed(() => !!binding.value);
|
2026-05-29 15:49:10 +08:00
|
|
|
|
|
2026-06-10 16:57:07 +08:00
|
|
|
|
const matchedFlows = computed(() => flowList.value);
|
2026-05-29 15:49:10 +08:00
|
|
|
|
|
|
|
|
|
|
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,
|
|
|
|
|
|
}))
|
|
|
|
|
|
);
|
|
|
|
|
|
|
2026-06-10 16:57:07 +08:00
|
|
|
|
watch(
|
|
|
|
|
|
() => currentRoute.value?.path,
|
|
|
|
|
|
async (path) => {
|
|
|
|
|
|
binding.value = null;
|
2026-05-29 15:49:10 +08:00
|
|
|
|
flowList.value = [];
|
2026-06-10 16:57:07 +08:00
|
|
|
|
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 },
|
|
|
|
|
|
);
|
2026-05-29 15:49:10 +08:00
|
|
|
|
|
|
|
|
|
|
function resetSelection() {
|
|
|
|
|
|
flowId.value = undefined;
|
|
|
|
|
|
bizDataId.value = undefined;
|
|
|
|
|
|
bizTitle.value = '';
|
|
|
|
|
|
recordList.value = [];
|
|
|
|
|
|
batchRows.value = [];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function openModal() {
|
2026-06-10 16:57:07 +08:00
|
|
|
|
if (!matchedFlows.value.length) {
|
|
|
|
|
|
createMessage.warning('当前页面暂无已发布的 MES 审批流,请先在审批流设计中发布');
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
2026-05-29 15:49:10 +08:00
|
|
|
|
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>
|