新增MES审批流设计功能,包括审批流定义、审批实例管理及审批办理接口,支持可视化设计与业务单据联动,提升审批流程的灵活性与用户体验。
This commit is contained in:
128
jeecgboot-vue3/src/views/approval/flow/ApprovalFlowList.vue
Normal file
128
jeecgboot-vue3/src/views/approval/flow/ApprovalFlowList.vue
Normal file
@@ -0,0 +1,128 @@
|
||||
<!--
|
||||
审批流设计 列表页
|
||||
@author GHT
|
||||
@date 2026-05-29 for:【QH-MES审批流设计】新增审批流可视化设计
|
||||
-->
|
||||
<template>
|
||||
<div class="p-2">
|
||||
<BasicTable @register="registerTable" :rowSelection="rowSelection">
|
||||
<template #tableTitle>
|
||||
<a-button type="primary" preIcon="ant-design:plus-outlined" @click="handleAdd" v-auth="'approval:flow:add'">新增审批流</a-button>
|
||||
<a-button
|
||||
v-if="selectedRowKeys.length > 0"
|
||||
danger
|
||||
preIcon="ant-design:delete-outlined"
|
||||
@click="handleBatchDelete"
|
||||
v-auth="'approval:flow:delete'"
|
||||
style="margin-left: 8px"
|
||||
>批量删除</a-button
|
||||
>
|
||||
</template>
|
||||
<template #action="{ record }">
|
||||
<TableAction :actions="getActions(record)" />
|
||||
</template>
|
||||
</BasicTable>
|
||||
|
||||
<!-- 基本信息弹窗 -->
|
||||
<ApprovalFlowModal @register="registerModal" @success="reload" />
|
||||
<!-- 流程设计器 -->
|
||||
<FlowDesign @register="registerDesign" @success="reload" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { BasicTable, TableAction } from '/@/components/Table';
|
||||
import { useModal } from '/@/components/Modal';
|
||||
import { useListPage } from '/@/hooks/system/useListPage';
|
||||
import { useMessage } from '/@/hooks/web/useMessage';
|
||||
import { columns, searchFormSchema } from './approvalFlow.data';
|
||||
import { getApprovalFlowList, deleteApprovalFlow, batchDeleteApprovalFlow, updateApprovalFlowStatus } from './approvalFlow.api';
|
||||
import ApprovalFlowModal from './ApprovalFlowModal.vue';
|
||||
import FlowDesign from './components/FlowDesign.vue';
|
||||
|
||||
defineOptions({ name: 'ApprovalFlowList' });
|
||||
|
||||
const { createMessage } = useMessage();
|
||||
const [registerModal, { openModal }] = useModal();
|
||||
const [registerDesign, { openModal: openDesign }] = useModal();
|
||||
|
||||
const { tableContext } = useListPage({
|
||||
tableProps: {
|
||||
title: '审批流列表',
|
||||
api: getApprovalFlowList,
|
||||
columns,
|
||||
formConfig: {
|
||||
schemas: searchFormSchema,
|
||||
labelWidth: 90,
|
||||
},
|
||||
actionColumn: {
|
||||
width: 240,
|
||||
fixed: 'right',
|
||||
},
|
||||
showIndexColumn: true,
|
||||
},
|
||||
});
|
||||
|
||||
const [registerTable, { reload }, { rowSelection, selectedRowKeys }] = tableContext;
|
||||
|
||||
function handleAdd() {
|
||||
openModal(true, { isUpdate: false });
|
||||
}
|
||||
|
||||
function handleEdit(record) {
|
||||
openModal(true, { isUpdate: true, record });
|
||||
}
|
||||
|
||||
// 打开可视化设计器
|
||||
function handleDesign(record, readonly = false) {
|
||||
openDesign(true, { record, readonly });
|
||||
}
|
||||
|
||||
function handleDelete(record) {
|
||||
deleteApprovalFlow({ id: record.id }, reload);
|
||||
}
|
||||
|
||||
function handleBatchDelete() {
|
||||
batchDeleteApprovalFlow({ ids: selectedRowKeys.value.join(',') }, reload);
|
||||
}
|
||||
|
||||
// 发布 / 停用
|
||||
async function handleToggleStatus(record) {
|
||||
const target = record.status === '1' ? '2' : '1';
|
||||
await updateApprovalFlowStatus({ id: record.id, status: target });
|
||||
createMessage.success(target === '1' ? '已发布' : '已停用');
|
||||
reload();
|
||||
}
|
||||
|
||||
function getActions(record) {
|
||||
return [
|
||||
{
|
||||
label: '设计',
|
||||
auth: 'approval:flow:design',
|
||||
onClick: handleDesign.bind(null, record, false),
|
||||
},
|
||||
{
|
||||
label: '编辑',
|
||||
auth: 'approval:flow:edit',
|
||||
onClick: handleEdit.bind(null, record),
|
||||
},
|
||||
{
|
||||
label: record.status === '1' ? '停用' : '发布',
|
||||
auth: 'approval:flow:design',
|
||||
popConfirm: {
|
||||
title: `确认${record.status === '1' ? '停用' : '发布'}该审批流?`,
|
||||
confirm: handleToggleStatus.bind(null, record),
|
||||
},
|
||||
},
|
||||
{
|
||||
label: '删除',
|
||||
color: 'error',
|
||||
auth: 'approval:flow:delete',
|
||||
popConfirm: {
|
||||
title: '确认删除该审批流?',
|
||||
confirm: handleDelete.bind(null, record),
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
</script>
|
||||
51
jeecgboot-vue3/src/views/approval/flow/ApprovalFlowModal.vue
Normal file
51
jeecgboot-vue3/src/views/approval/flow/ApprovalFlowModal.vue
Normal file
@@ -0,0 +1,51 @@
|
||||
<!--
|
||||
审批流 基本信息 新增/编辑弹窗(先选单据)
|
||||
@author GHT
|
||||
@date 2026-05-29 for:【QH-MES审批流设计】新增审批流可视化设计
|
||||
-->
|
||||
<template>
|
||||
<BasicModal v-bind="$attrs" @register="registerModal" :title="title" :width="560" @ok="handleSubmit">
|
||||
<BasicForm @register="registerForm" />
|
||||
</BasicModal>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, ref, unref } from 'vue';
|
||||
import { BasicModal, useModalInner } from '/@/components/Modal';
|
||||
import { BasicForm, useForm } from '/@/components/Form/index';
|
||||
import { formSchema } from './approvalFlow.data';
|
||||
import { saveOrUpdateApprovalFlow, getApprovalFlowById } from './approvalFlow.api';
|
||||
|
||||
const emit = defineEmits(['success', 'register']);
|
||||
|
||||
const isUpdate = ref(true);
|
||||
const title = computed(() => (unref(isUpdate) ? '编辑审批流' : '新增审批流'));
|
||||
|
||||
const [registerForm, { resetFields, setFieldsValue, validate }] = useForm({
|
||||
schemas: formSchema,
|
||||
showActionButtonGroup: false,
|
||||
labelWidth: 100,
|
||||
});
|
||||
|
||||
const [registerModal, { setModalProps, closeModal }] = useModalInner(async (data) => {
|
||||
await resetFields();
|
||||
setModalProps({ confirmLoading: false });
|
||||
isUpdate.value = !!data?.isUpdate;
|
||||
if (unref(isUpdate) && data?.record?.id) {
|
||||
const record = await getApprovalFlowById({ id: data.record.id });
|
||||
await setFieldsValue({ ...record });
|
||||
}
|
||||
});
|
||||
|
||||
async function handleSubmit() {
|
||||
try {
|
||||
const values = await validate();
|
||||
setModalProps({ confirmLoading: true });
|
||||
await saveOrUpdateApprovalFlow(values, unref(isUpdate));
|
||||
closeModal();
|
||||
emit('success');
|
||||
} finally {
|
||||
setModalProps({ confirmLoading: false });
|
||||
}
|
||||
}
|
||||
</script>
|
||||
75
jeecgboot-vue3/src/views/approval/flow/approvalFlow.api.ts
Normal file
75
jeecgboot-vue3/src/views/approval/flow/approvalFlow.api.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
import { defHttp } from '/@/utils/http/axios';
|
||||
|
||||
/**
|
||||
* 审批流设计 API
|
||||
* @author GHT
|
||||
* @date 2026-05-29 for:【QH-MES审批流设计】新增审批流可视化设计
|
||||
*/
|
||||
enum Api {
|
||||
list = '/xslmes/approvalFlow/list',
|
||||
save = '/xslmes/approvalFlow/add',
|
||||
edit = '/xslmes/approvalFlow/edit',
|
||||
get = '/xslmes/approvalFlow/queryById',
|
||||
saveDesign = '/xslmes/approvalFlow/saveDesign',
|
||||
updateStatus = '/xslmes/approvalFlow/updateStatus',
|
||||
delete = '/xslmes/approvalFlow/delete',
|
||||
deleteBatch = '/xslmes/approvalFlow/deleteBatch',
|
||||
// update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】当前页设计上下文-----
|
||||
designContext = '/xslmes/approvalFlow/designContext',
|
||||
// update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】当前页设计上下文-----
|
||||
}
|
||||
|
||||
/**
|
||||
* 分页列表查询
|
||||
*/
|
||||
export const getApprovalFlowList = (params) => defHttp.get({ url: Api.list, params });
|
||||
|
||||
/**
|
||||
* 新增/编辑基本信息
|
||||
*/
|
||||
export const saveOrUpdateApprovalFlow = (params, isUpdate) => {
|
||||
const url = isUpdate ? Api.edit : Api.save;
|
||||
return defHttp.post({ url, params });
|
||||
};
|
||||
|
||||
/**
|
||||
* 通过 id 查询(含流程设计 JSON)
|
||||
*/
|
||||
export const getApprovalFlowById = (params) => defHttp.get({ url: Api.get, params });
|
||||
|
||||
/**
|
||||
* 保存流程设计(节点树 JSON)
|
||||
*/
|
||||
export const saveApprovalFlowDesign = (params) => defHttp.post({ url: Api.saveDesign, params });
|
||||
|
||||
/**
|
||||
* 发布 / 停用
|
||||
*/
|
||||
export const updateApprovalFlowStatus = (params) => defHttp.post({ url: Api.updateStatus, params }, { joinParamsToUrl: true });
|
||||
|
||||
/**
|
||||
* 删除
|
||||
*/
|
||||
export const deleteApprovalFlow = (params, handleSuccess) => {
|
||||
return defHttp.delete({ url: Api.delete, data: params }, { joinParamsToUrl: true }).then(() => {
|
||||
handleSuccess();
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 批量删除
|
||||
*/
|
||||
export const batchDeleteApprovalFlow = (params, handleSuccess) => {
|
||||
return defHttp.delete({ url: Api.deleteBatch, data: params }, { joinParamsToUrl: true }).then(() => {
|
||||
handleSuccess();
|
||||
});
|
||||
};
|
||||
|
||||
// update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】当前页设计上下文(解析阶段字段+取/建草稿流程)-----
|
||||
/**
|
||||
* 获取当前功能页的审批流设计上下文:
|
||||
* 返回 { routePath, bizTable, bizTableName, stages[], flow },
|
||||
* stages 为识别到的阶段字段(校对/审核/审批/分发/抄送),flow 为可直接进入设计器的流程记录。
|
||||
*/
|
||||
export const getApprovalDesignContext = (routePath: string) => defHttp.get({ url: Api.designContext, params: { routePath } });
|
||||
// update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】当前页设计上下文(解析阶段字段+取/建草稿流程)-----
|
||||
112
jeecgboot-vue3/src/views/approval/flow/approvalFlow.data.ts
Normal file
112
jeecgboot-vue3/src/views/approval/flow/approvalFlow.data.ts
Normal file
@@ -0,0 +1,112 @@
|
||||
import { BasicColumn, FormSchema } from '/@/components/Table';
|
||||
|
||||
/**
|
||||
* 审批流设计 列表列 / 表单 schema
|
||||
* @author GHT
|
||||
* @date 2026-05-29 for:【QH-MES审批流设计】新增审批流可视化设计
|
||||
*/
|
||||
export const columns: BasicColumn[] = [
|
||||
{
|
||||
title: '审批流名称',
|
||||
dataIndex: 'flowName',
|
||||
align: 'left',
|
||||
},
|
||||
{
|
||||
title: '绑定单据',
|
||||
dataIndex: 'bizTableName',
|
||||
width: 200,
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'status_dictText',
|
||||
width: 100,
|
||||
},
|
||||
{
|
||||
title: '备注',
|
||||
dataIndex: 'remark',
|
||||
align: 'left',
|
||||
},
|
||||
{
|
||||
title: '创建时间',
|
||||
dataIndex: 'createTime',
|
||||
width: 180,
|
||||
},
|
||||
];
|
||||
|
||||
export const searchFormSchema: FormSchema[] = [
|
||||
{
|
||||
field: 'flowName',
|
||||
label: '审批流名称',
|
||||
component: 'Input',
|
||||
colProps: { span: 6 },
|
||||
},
|
||||
{
|
||||
field: 'bizTable',
|
||||
label: '绑定单据',
|
||||
component: 'JDictSelectTag',
|
||||
componentProps: {
|
||||
dictCode: 'mes_xsl_approval_biz_doc',
|
||||
},
|
||||
colProps: { span: 6 },
|
||||
},
|
||||
{
|
||||
field: 'status',
|
||||
label: '状态',
|
||||
component: 'JDictSelectTag',
|
||||
componentProps: {
|
||||
dictCode: 'mes_xsl_approval_flow_status',
|
||||
},
|
||||
colProps: { span: 6 },
|
||||
},
|
||||
];
|
||||
|
||||
export const formSchema: FormSchema[] = [
|
||||
{
|
||||
label: '主键',
|
||||
field: 'id',
|
||||
component: 'Input',
|
||||
show: false,
|
||||
},
|
||||
{
|
||||
field: 'flowName',
|
||||
label: '审批流名称',
|
||||
component: 'Input',
|
||||
required: true,
|
||||
componentProps: {
|
||||
placeholder: '请输入审批流名称',
|
||||
maxlength: 100,
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'bizTable',
|
||||
label: '绑定单据',
|
||||
component: 'JDictSelectTag',
|
||||
required: true,
|
||||
componentProps: {
|
||||
dictCode: 'mes_xsl_approval_biz_doc',
|
||||
placeholder: '请先选择需要审批的单据',
|
||||
},
|
||||
// 编辑时禁止修改绑定单据,避免已设计的节点条件与单据字段错配
|
||||
dynamicDisabled: ({ values }) => !!values.id,
|
||||
},
|
||||
{
|
||||
field: 'titleField',
|
||||
label: '单据标题字段',
|
||||
component: 'Input',
|
||||
helpMessage: '发起审批时用于展示具体单据的字段名(如 spec_name、code),不填则只显示单据ID',
|
||||
componentProps: {
|
||||
placeholder: '选填,业务表中用于展示的字段名,如 spec_name',
|
||||
maxlength: 100,
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'remark',
|
||||
label: '备注',
|
||||
component: 'InputTextArea',
|
||||
componentProps: {
|
||||
placeholder: '请输入备注',
|
||||
maxlength: 500,
|
||||
rows: 3,
|
||||
},
|
||||
},
|
||||
];
|
||||
25
jeecgboot-vue3/src/views/approval/flow/approvalHandle.api.ts
Normal file
25
jeecgboot-vue3/src/views/approval/flow/approvalHandle.api.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { defHttp } from '/@/utils/http/axios';
|
||||
|
||||
/**
|
||||
* 审批办理/流转 API(供 IM 审批卡片按钮调用)
|
||||
* @author GHT
|
||||
* @date 2026-05-29 for:【QH-MES审批流设计】审批办理/流转
|
||||
*/
|
||||
enum Api {
|
||||
detail = '/xslmes/approvalHandle/detail',
|
||||
status = '/xslmes/approvalHandle/status',
|
||||
approve = '/xslmes/approvalHandle/approve',
|
||||
reject = '/xslmes/approvalHandle/reject',
|
||||
}
|
||||
|
||||
/** 查看单据全部字段 + 审批进度/历史 */
|
||||
export const getApprovalDetail = (instanceId: string) => defHttp.get({ url: Api.detail, params: { instanceId } });
|
||||
|
||||
/** 轻量实时状态:用于卡片判断是否仍可办理(旧节点卡片置灰) */
|
||||
export const getApprovalStatus = (instanceId: string) => defHttp.get({ url: Api.status, params: { instanceId } });
|
||||
|
||||
/** 审批通过(按节点 multiMode 流转到下一处理人/节点) */
|
||||
export const approveApproval = (params: { instanceId: string; comment?: string }) => defHttp.post({ url: Api.approve, data: params });
|
||||
|
||||
/** 驳回(需填写理由) */
|
||||
export const rejectApproval = (params: { instanceId: string; reason: string }) => defHttp.post({ url: Api.reject, data: params });
|
||||
189
jeecgboot-vue3/src/views/approval/flow/components/FlowDesign.vue
Normal file
189
jeecgboot-vue3/src/views/approval/flow/components/FlowDesign.vue
Normal file
@@ -0,0 +1,189 @@
|
||||
<!--
|
||||
钉钉式审批流 可视化设计器(全屏)
|
||||
@author GHT
|
||||
@date 2026-05-29 for:【QH-MES审批流设计】新增审批流可视化设计
|
||||
-->
|
||||
<template>
|
||||
<BasicModal v-bind="$attrs" @register="registerModal" :title="modalTitle" defaultFullscreen :canFullscreen="false" :showOkBtn="!readonly" :okText="'保存并发布'" @ok="handleSave" :wrapClassName="flowApiRecording ? 'flow-design-recording-hide' : ''">
|
||||
<div class="fd-design">
|
||||
<div class="fd-toolbar">
|
||||
<span class="fd-tb-item">绑定单据:<b>{{ record.bizTableName || record.bizTable }}</b></span>
|
||||
<span class="fd-tb-item">审批流:<b>{{ record.flowName }}</b></span>
|
||||
<span class="fd-tb-tip">点击节点可配置,点击节点间的「+」可插入审批人 / 抄送人 / 条件分支</span>
|
||||
</div>
|
||||
<!-- update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】当前页识别到的阶段字段作为候选,点选追加为流程节点----- -->
|
||||
<div class="fd-body">
|
||||
<div class="fd-palette" v-if="!readonly && paletteStages.length">
|
||||
<div class="fd-palette-title">当前页识别到的审批阶段</div>
|
||||
<div class="fd-palette-tip">点击下方阶段,按顺序追加到流程末尾;处理人将取自单据对应字段。</div>
|
||||
<div class="fd-palette-list">
|
||||
<div v-for="s in paletteStages" :key="s.stageKey" class="fd-palette-item" :class="'fd-palette-' + s.nodeType" @click="appendStageNode(s)">
|
||||
<div class="fd-palette-item-name">{{ s.stageName }}</div>
|
||||
<div class="fd-palette-item-field">{{ s.fieldComment || s.field }}</div>
|
||||
<Icon icon="ant-design:plus-circle-outlined" class="fd-palette-item-add" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="fd-canvas">
|
||||
<div class="fd-flow" v-if="root">
|
||||
<FlowNode :node="root" />
|
||||
<div class="fd-end">
|
||||
<div class="fd-end-dot"></div>
|
||||
<span>流程结束</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】当前页识别到的阶段字段作为候选,点选追加为流程节点----- -->
|
||||
</div>
|
||||
<NodeConfigDrawer ref="drawerRef" :readonly="readonly" />
|
||||
</BasicModal>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, provide, reactive, ref } from 'vue';
|
||||
import { BasicModal, useModalInner } from '/@/components/Modal';
|
||||
import { useMessage } from '/@/hooks/web/useMessage';
|
||||
import FlowNode from './FlowNode.vue';
|
||||
import NodeConfigDrawer from './NodeConfigDrawer.vue';
|
||||
import {
|
||||
createStartNode,
|
||||
createApproverNode,
|
||||
createCcNode,
|
||||
createConditionNode,
|
||||
createStageNode,
|
||||
insertAfter,
|
||||
removeNode,
|
||||
addBranch,
|
||||
} from './flowTypes';
|
||||
import type { FlowNode as FlowNodeType, NodeType, StageField } from './flowTypes';
|
||||
import { saveApprovalFlowDesign, getApprovalFlowById } from '../approvalFlow.api';
|
||||
import { flowApiRecording } from '/@/utils/flowApiRecorder';
|
||||
|
||||
defineOptions({ name: 'ApprovalFlowDesign' });
|
||||
|
||||
const emit = defineEmits(['success', 'register']);
|
||||
const { createMessage } = useMessage();
|
||||
|
||||
const root = ref<FlowNodeType | null>(null);
|
||||
const readonly = ref(false);
|
||||
const record = reactive<any>({ id: '', flowName: '', bizTable: '', bizTableName: '' });
|
||||
const drawerRef = ref();
|
||||
// update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】当前页识别到的候选阶段字段----- -->
|
||||
const paletteStages = ref<StageField[]>([]);
|
||||
// update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】当前页识别到的候选阶段字段----- -->
|
||||
|
||||
const modalTitle = computed(() => (readonly.value ? '查看审批流' : '设计审批流'));
|
||||
|
||||
// 设计器上下文,提供给递归 FlowNode 使用
|
||||
const flowCtx = reactive({
|
||||
readonly: false,
|
||||
onSelect: (node: FlowNodeType) => {
|
||||
drawerRef.value?.openDrawer(node);
|
||||
},
|
||||
onInsert: (prevId: string, type: NodeType) => {
|
||||
if (!root.value) return;
|
||||
const factory: Record<string, () => FlowNodeType> = {
|
||||
approver: createApproverNode,
|
||||
cc: createCcNode,
|
||||
condition: createConditionNode,
|
||||
};
|
||||
const node = factory[type]?.();
|
||||
if (node) insertAfter(root.value, prevId, node);
|
||||
},
|
||||
onDelete: (id: string) => {
|
||||
if (root.value) removeNode(root.value, id);
|
||||
},
|
||||
addBranch: (conditionId: string) => {
|
||||
if (root.value) addBranch(root.value, conditionId);
|
||||
},
|
||||
});
|
||||
|
||||
// 通过 provide 注入,供递归 FlowNode 调用(避免逐层透传)
|
||||
provide('flowCtx', flowCtx);
|
||||
|
||||
const [registerModal, { setModalProps, closeModal }] = useModalInner(async (data) => {
|
||||
readonly.value = !!data?.readonly;
|
||||
flowCtx.readonly = readonly.value;
|
||||
// update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】接收当前页解析出的候选阶段字段----- -->
|
||||
paletteStages.value = Array.isArray(data?.paletteStages) ? data.paletteStages : [];
|
||||
// update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】接收当前页解析出的候选阶段字段----- -->
|
||||
const id = data?.record?.id || '';
|
||||
Object.assign(record, {
|
||||
id,
|
||||
flowName: data?.record?.flowName || '',
|
||||
bizTable: data?.record?.bizTable || '',
|
||||
bizTableName: data?.record?.bizTableName || '',
|
||||
});
|
||||
// 列表接口未返回大字段 flow_config,需按 id 重新查询完整记录
|
||||
let cfg = data?.record?.flowConfig;
|
||||
if (id) {
|
||||
try {
|
||||
setModalProps({ loading: true });
|
||||
const full = await getApprovalFlowById({ id });
|
||||
if (full) {
|
||||
cfg = full.flowConfig;
|
||||
Object.assign(record, {
|
||||
flowName: full.flowName || record.flowName,
|
||||
bizTable: full.bizTable || record.bizTable,
|
||||
bizTableName: full.bizTableName || record.bizTableName,
|
||||
});
|
||||
}
|
||||
} finally {
|
||||
setModalProps({ loading: false });
|
||||
}
|
||||
}
|
||||
// 解析已有流程设计,无则初始化一个发起人节点
|
||||
root.value = parseConfig(cfg);
|
||||
});
|
||||
|
||||
function parseConfig(cfg?: string): FlowNodeType {
|
||||
if (cfg) {
|
||||
try {
|
||||
const obj = JSON.parse(cfg);
|
||||
if (obj && obj.type) return obj;
|
||||
} catch (e) {
|
||||
console.warn('[审批流设计] flowConfig 解析失败,重置为默认', e);
|
||||
}
|
||||
}
|
||||
return createStartNode();
|
||||
}
|
||||
|
||||
// update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】点选候选阶段,追加为流程末尾节点----- -->
|
||||
/** 将候选阶段追加到主流程链末尾(沿 childNode 找到尾节点后插入) */
|
||||
function appendStageNode(stage: StageField) {
|
||||
if (!root.value || readonly.value) return;
|
||||
let tail: FlowNodeType = root.value;
|
||||
while (tail.childNode) {
|
||||
tail = tail.childNode;
|
||||
}
|
||||
const node = createStageNode(stage);
|
||||
insertAfter(root.value, tail.id, node);
|
||||
createMessage.success(`已添加「${stage.stageName}」节点`);
|
||||
}
|
||||
// update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】点选候选阶段,追加为流程末尾节点----- -->
|
||||
|
||||
async function handleSave() {
|
||||
if (!record.id) {
|
||||
createMessage.error('缺少审批流ID,无法保存');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
setModalProps({ confirmLoading: true });
|
||||
await saveApprovalFlowDesign({
|
||||
id: record.id,
|
||||
flowConfig: JSON.stringify(root.value),
|
||||
status: '1',
|
||||
});
|
||||
createMessage.success('流程设计已保存并发布');
|
||||
closeModal();
|
||||
emit('success');
|
||||
} finally {
|
||||
setModalProps({ confirmLoading: false });
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less">
|
||||
@import './flow.less';
|
||||
</style>
|
||||
@@ -0,0 +1,99 @@
|
||||
<!--
|
||||
钉钉式审批流 递归节点组件
|
||||
@author GHT
|
||||
@date 2026-05-29 for:【QH-MES审批流设计】新增审批流可视化设计
|
||||
-->
|
||||
<template>
|
||||
<div class="fd-node">
|
||||
<!-- 条件分支节点:渲染多列分支 -->
|
||||
<div v-if="node.type === 'condition'" class="fd-branches">
|
||||
<div class="fd-add-branch-wrap">
|
||||
<a-button size="small" type="primary" ghost class="fd-add-branch" @click="ctx.addBranch(node.id)">+ 条件</a-button>
|
||||
</div>
|
||||
<div class="fd-branch-cols">
|
||||
<div class="fd-branch-col" v-for="(b, idx) in node.conditionNodes" :key="b.id">
|
||||
<!-- 首末列外侧连线遮罩,形成分支汇聚效果 -->
|
||||
<div v-if="idx === 0" class="fd-cover fd-cover-tl"></div>
|
||||
<div v-if="idx === 0" class="fd-cover fd-cover-bl"></div>
|
||||
<div v-if="idx === lastBranchIndex" class="fd-cover fd-cover-tr"></div>
|
||||
<div v-if="idx === lastBranchIndex" class="fd-cover fd-cover-br"></div>
|
||||
<div class="fd-branch-inner">
|
||||
<FlowNode :node="b" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 普通卡片:start / approver / cc / branch -->
|
||||
<div v-else class="fd-card-wrap">
|
||||
<div class="fd-card" :class="['fd-' + node.type, { 'fd-error': isPlaceholder }]" @click="ctx.onSelect(node)">
|
||||
<div class="fd-card-header">
|
||||
<span class="fd-title">{{ node.name }}</span>
|
||||
<span v-if="node.type === 'branch' && !node.props.isDefault" class="fd-priority">优先级 {{ node.props.priorityLevel }}</span>
|
||||
<Icon v-if="canDelete && !ctx.readonly" icon="ant-design:close-outlined" class="fd-del" @click.stop="ctx.onDelete(node.id)" />
|
||||
</div>
|
||||
<div class="fd-card-body" :class="{ 'fd-placeholder': isPlaceholder }">{{ summary }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 加号:在当前节点之后插入新节点 -->
|
||||
<div v-if="!ctx.readonly" class="fd-add">
|
||||
<a-popover trigger="click" placement="rightTop" v-model:open="addOpen" :overlayClassName="'fd-add-pop'">
|
||||
<template #content>
|
||||
<div class="fd-add-menu">
|
||||
<div class="fd-add-item fd-add-approver" @click="add('approver')">
|
||||
<Icon icon="ant-design:audit-outlined" /> <span>审批人</span>
|
||||
</div>
|
||||
<div class="fd-add-item fd-add-cc" @click="add('cc')">
|
||||
<Icon icon="ant-design:mail-outlined" /> <span>抄送人</span>
|
||||
</div>
|
||||
<div class="fd-add-item fd-add-condition" @click="add('condition')">
|
||||
<Icon icon="ant-design:share-alt-outlined" /> <span>条件分支</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<button class="fd-add-btn" title="添加节点"><Icon icon="ant-design:plus-outlined" /></button>
|
||||
</a-popover>
|
||||
</div>
|
||||
|
||||
<!-- 后续链(递归) -->
|
||||
<FlowNode v-if="node.childNode" :node="node.childNode" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, inject, ref } from 'vue';
|
||||
import { nodeSummary } from './flowTypes';
|
||||
import type { FlowNode as FlowNodeType, NodeType } from './flowTypes';
|
||||
|
||||
defineOptions({ name: 'FlowNode' });
|
||||
|
||||
const props = defineProps<{ node: FlowNodeType }>();
|
||||
|
||||
// 注入设计器上下文
|
||||
const ctx = inject<any>('flowCtx', {
|
||||
readonly: false,
|
||||
onSelect: () => {},
|
||||
onInsert: () => {},
|
||||
onDelete: () => {},
|
||||
addBranch: () => {},
|
||||
});
|
||||
|
||||
const addOpen = ref(false);
|
||||
|
||||
const summary = computed(() => nodeSummary(props.node));
|
||||
|
||||
// 条件分支最后一列索引(用于首末列连线遮罩)
|
||||
const lastBranchIndex = computed(() => (props.node.conditionNodes?.length || 0) - 1);
|
||||
|
||||
// 摘要为"请设置..."视为未配置(红色提示)
|
||||
const isPlaceholder = computed(() => summary.value.startsWith('请'));
|
||||
|
||||
// 发起人节点不可删除
|
||||
const canDelete = computed(() => props.node.type !== 'start');
|
||||
|
||||
function add(type: NodeType) {
|
||||
ctx.onInsert(props.node.id, type);
|
||||
addOpen.value = false;
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,359 @@
|
||||
<!--
|
||||
审批流节点配置抽屉
|
||||
@author GHT
|
||||
@date 2026-05-29 for:【QH-MES审批流设计】新增审批流可视化设计
|
||||
-->
|
||||
<template>
|
||||
<a-drawer :title="title" :width="480" :open="open && !flowApiRecording" @close="onClose" :maskClosable="!readonly">
|
||||
<template v-if="form">
|
||||
<a-form layout="vertical">
|
||||
<a-form-item label="节点名称">
|
||||
<a-input v-model:value="form.name" :disabled="readonly || node?.type === 'start'" placeholder="请输入节点名称" />
|
||||
</a-form-item>
|
||||
|
||||
<!-- 发起人 -->
|
||||
<template v-if="node?.type === 'start'">
|
||||
<a-form-item label="可发起人员">
|
||||
<a-radio-group v-model:value="form.props.initiatorType" :disabled="readonly">
|
||||
<a-radio value="all">所有人</a-radio>
|
||||
<a-radio value="user">指定成员</a-radio>
|
||||
<a-radio value="role">指定角色</a-radio>
|
||||
</a-radio-group>
|
||||
</a-form-item>
|
||||
<a-form-item v-if="form.props.initiatorType === 'user'" label="指定成员">
|
||||
<JSelectUser v-model:value="form.props.userText" :disabled="readonly" />
|
||||
</a-form-item>
|
||||
<a-form-item v-if="form.props.initiatorType === 'role'" label="指定角色">
|
||||
<ApiSelect mode="multiple" v-model:value="form.props.roleList" :api="roleApi" :params="{ pageSize: 1000 }" resultField="records" labelField="roleName" valueField="id" :disabled="readonly" placeholder="请选择角色" />
|
||||
</a-form-item>
|
||||
</template>
|
||||
|
||||
<!-- 审批人 -->
|
||||
<template v-else-if="node?.type === 'approver'">
|
||||
<a-form-item label="审批人类型">
|
||||
<a-radio-group v-model:value="form.props.approverType" :disabled="readonly">
|
||||
<a-radio value="user">指定成员</a-radio>
|
||||
<a-radio value="role">指定角色</a-radio>
|
||||
<a-radio value="leader">主管</a-radio>
|
||||
<a-radio value="self">发起人自己</a-radio>
|
||||
<!-- update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】取单据字段审批人----- -->
|
||||
<a-radio value="field">取单据字段</a-radio>
|
||||
<!-- update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】取单据字段审批人----- -->
|
||||
</a-radio-group>
|
||||
</a-form-item>
|
||||
<!-- update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】取单据字段审批人配置----- -->
|
||||
<template v-if="form.props.approverType === 'field'">
|
||||
<a-alert type="info" show-icon style="margin-bottom: 12px" message="发起审批时,处理人将取自单据该字段的值(通常为人员账号)。" />
|
||||
<a-form-item label="字段中文名">
|
||||
<a-input v-model:value="form.props.fieldLabel" :disabled="readonly" placeholder="如:校对人" />
|
||||
</a-form-item>
|
||||
<a-form-item label="字段名">
|
||||
<a-input v-model:value="form.props.fieldName" :disabled="readonly" placeholder="如:proofreader" />
|
||||
</a-form-item>
|
||||
</template>
|
||||
<!-- update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】取单据字段审批人配置----- -->
|
||||
<a-form-item v-if="form.props.approverType === 'user'" label="指定成员">
|
||||
<JSelectUser v-model:value="form.props.userText" :disabled="readonly" />
|
||||
</a-form-item>
|
||||
<a-form-item v-if="form.props.approverType === 'role'" label="指定角色">
|
||||
<ApiSelect mode="multiple" v-model:value="form.props.roleList" :api="roleApi" :params="{ pageSize: 1000 }" resultField="records" labelField="roleName" valueField="id" :disabled="readonly" placeholder="请选择角色" />
|
||||
</a-form-item>
|
||||
<a-form-item v-if="form.props.approverType === 'leader'" label="主管层级">
|
||||
<a-select v-model:value="form.props.leaderLevel" :disabled="readonly" style="width: 160px">
|
||||
<a-select-option :value="1">直接主管</a-select-option>
|
||||
<a-select-option :value="2">第2级主管</a-select-option>
|
||||
<a-select-option :value="3">第3级主管</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label="多人审批方式">
|
||||
<a-radio-group v-model:value="form.props.multiMode" :disabled="readonly">
|
||||
<a-radio value="and">会签(需全部同意)</a-radio>
|
||||
<a-radio value="or">或签(一人同意)</a-radio>
|
||||
<a-radio value="sequence">依次审批</a-radio>
|
||||
</a-radio-group>
|
||||
</a-form-item>
|
||||
<a-form-item label="审批人为空时">
|
||||
<a-radio-group v-model:value="form.props.emptyStrategy" :disabled="readonly">
|
||||
<a-radio value="admin">转交管理员</a-radio>
|
||||
<a-radio value="pass">自动通过</a-radio>
|
||||
<a-radio value="stop">终止流程</a-radio>
|
||||
</a-radio-group>
|
||||
</a-form-item>
|
||||
|
||||
<!-- update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】节点回调接口可视化配置(录制业务按钮接口)----- -->
|
||||
<a-divider style="margin: 16px 0 12px">回调接口(审批联动业务)</a-divider>
|
||||
<a-alert
|
||||
type="info"
|
||||
show-icon
|
||||
style="margin-bottom: 12px"
|
||||
message="审批到对应时机时,系统会以「当前处理人」身份调用所选业务接口(自动带上单据ID)。点击「录制」后,设计器会临时隐藏,请到页面上点击目标按钮(如“批准”),系统自动识别其接口并回填。"
|
||||
/>
|
||||
<div v-for="phase in callbackPhases" :key="phase.key" class="fd-cb-block">
|
||||
<div class="fd-cb-title">{{ phase.label }}</div>
|
||||
<div v-for="(a, i) in form.props.callbackActions[phase.key]" :key="i" class="fd-cb-row">
|
||||
<a-input v-model:value="a.name" placeholder="动作名" :disabled="readonly" style="width: 88px" />
|
||||
<a-select v-model:value="a.method" :disabled="readonly" style="width: 82px" :options="methodOptions" />
|
||||
<a-input v-model:value="a.url" placeholder="接口路径 /xxx" :disabled="readonly" style="flex: 1; min-width: 120px" />
|
||||
<Icon v-if="!readonly" icon="ant-design:minus-circle-outlined" class="fd-cb-del" @click="removeAction(phase.key, i)" />
|
||||
</div>
|
||||
<a-space v-if="!readonly" style="margin-top: 4px">
|
||||
<a-button size="small" @click="recordInto(phase.key)">
|
||||
<Icon icon="ant-design:aim-outlined" />
|
||||
<span>录制接口</span>
|
||||
</a-button>
|
||||
<a-button size="small" @click="addAction(phase.key)">手动添加</a-button>
|
||||
</a-space>
|
||||
</div>
|
||||
<!-- update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】节点回调接口可视化配置(录制业务按钮接口)----- -->
|
||||
</template>
|
||||
|
||||
<!-- 抄送人 -->
|
||||
<template v-else-if="node?.type === 'cc'">
|
||||
<!-- update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】抄送人来源支持取单据字段----- -->
|
||||
<a-form-item label="抄送人来源">
|
||||
<a-radio-group v-model:value="form.props.ccType" :disabled="readonly">
|
||||
<a-radio value="user">指定成员/角色</a-radio>
|
||||
<a-radio value="field">取单据字段</a-radio>
|
||||
</a-radio-group>
|
||||
</a-form-item>
|
||||
<template v-if="form.props.ccType === 'field'">
|
||||
<a-alert type="info" show-icon style="margin-bottom: 12px" message="发起审批时,抄送人将取自单据该字段的值(通常为人员账号)。" />
|
||||
<a-form-item label="字段中文名">
|
||||
<a-input v-model:value="form.props.fieldLabel" :disabled="readonly" placeholder="如:分发人" />
|
||||
</a-form-item>
|
||||
<a-form-item label="字段名">
|
||||
<a-input v-model:value="form.props.fieldName" :disabled="readonly" placeholder="如:distributor" />
|
||||
</a-form-item>
|
||||
</template>
|
||||
<template v-else>
|
||||
<a-form-item label="抄送成员">
|
||||
<JSelectUser v-model:value="form.props.userText" :disabled="readonly" />
|
||||
</a-form-item>
|
||||
<a-form-item label="抄送角色">
|
||||
<ApiSelect mode="multiple" v-model:value="form.props.roleList" :api="roleApi" :params="{ pageSize: 1000 }" resultField="records" labelField="roleName" valueField="id" :disabled="readonly" placeholder="请选择角色" />
|
||||
</a-form-item>
|
||||
</template>
|
||||
<a-form-item>
|
||||
<a-checkbox v-model:checked="form.props.allowEditCc" :disabled="readonly">允许审批人自行添加抄送人</a-checkbox>
|
||||
</a-form-item>
|
||||
<!-- update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】抄送人来源支持取单据字段----- -->
|
||||
</template>
|
||||
|
||||
<!-- 条件分支 -->
|
||||
<template v-else-if="node?.type === 'branch'">
|
||||
<template v-if="form.props.isDefault">
|
||||
<a-alert type="info" show-icon message="“其它情况”分支:当以上条件均不满足时进入此分支,无需配置条件。" />
|
||||
</template>
|
||||
<template v-else>
|
||||
<a-form-item label="条件关系">
|
||||
<a-radio-group v-model:value="form.props.logic" :disabled="readonly">
|
||||
<a-radio value="and">且(同时满足)</a-radio>
|
||||
<a-radio value="or">或(满足其一)</a-radio>
|
||||
</a-radio-group>
|
||||
</a-form-item>
|
||||
<a-form-item label="条件设置">
|
||||
<div v-for="(c, i) in form.props.conditions" :key="i" class="fd-cond-row">
|
||||
<a-input v-model:value="c.label" placeholder="字段中文名" :disabled="readonly" style="width: 110px" />
|
||||
<a-input v-model:value="c.field" placeholder="字段名" :disabled="readonly" style="width: 110px" />
|
||||
<a-select v-model:value="c.operator" :disabled="readonly" style="width: 100px" :options="operatorOptions" />
|
||||
<a-input v-if="!['empty', 'notEmpty'].includes(c.operator)" v-model:value="c.value" placeholder="值" :disabled="readonly" style="width: 90px" />
|
||||
<Icon v-if="!readonly" icon="ant-design:minus-circle-outlined" class="fd-cond-del" @click="removeCond(i)" />
|
||||
</div>
|
||||
<a-button v-if="!readonly" type="dashed" block @click="addCond" style="margin-top: 8px">+ 添加条件</a-button>
|
||||
</a-form-item>
|
||||
</template>
|
||||
</template>
|
||||
</a-form>
|
||||
</template>
|
||||
|
||||
<template #footer v-if="!readonly">
|
||||
<a-space>
|
||||
<a-button @click="onClose">取消</a-button>
|
||||
<a-button type="primary" @click="onConfirm">确定</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
</a-drawer>
|
||||
|
||||
<!-- update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】录制中悬浮提示条----- -->
|
||||
<Teleport to="body">
|
||||
<div v-if="flowApiRecording" class="flow-record-banner">
|
||||
<Icon icon="ant-design:aim-outlined" class="flow-record-banner-icon" />
|
||||
<span class="flow-record-banner-text">录制中:请点击页面上要绑定的业务按钮(如“批准/反审核”),系统会自动识别其接口</span>
|
||||
<a-button size="small" danger @click="cancelRecord">取消录制</a-button>
|
||||
</div>
|
||||
</Teleport>
|
||||
<!-- update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】录制中悬浮提示条----- -->
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, ref } from 'vue';
|
||||
import { cloneDeep } from 'lodash-es';
|
||||
import { defHttp } from '/@/utils/http/axios';
|
||||
import { ApiSelect } from '/@/components/Form';
|
||||
import JSelectUser from '/@/components/Form/src/jeecg/components/JSelectUser.vue';
|
||||
import { OPERATOR_OPTIONS } from './flowTypes';
|
||||
import type { FlowNode } from './flowTypes';
|
||||
import { useMessage } from '/@/hooks/web/useMessage';
|
||||
import { flowApiRecording, startFlowApiRecord, cancelFlowApiRecord } from '/@/utils/flowApiRecorder';
|
||||
|
||||
const props = defineProps<{ readonly?: boolean }>();
|
||||
const emit = defineEmits(['confirm']);
|
||||
|
||||
const { createMessage } = useMessage();
|
||||
|
||||
const open = ref(false);
|
||||
const node = ref<FlowNode | null>(null);
|
||||
const form = ref<any>(null);
|
||||
|
||||
const operatorOptions = OPERATOR_OPTIONS;
|
||||
const readonly = computed(() => !!props.readonly);
|
||||
|
||||
// 回调接口配置:三个触发时机
|
||||
const callbackPhases = [
|
||||
{ key: 'onNodeApprove', label: '本节点通过时执行' },
|
||||
{ key: 'onApprove', label: '流程最终通过时执行' },
|
||||
{ key: 'onReject', label: '驳回时执行' },
|
||||
];
|
||||
const methodOptions = [
|
||||
{ label: 'POST', value: 'POST' },
|
||||
{ label: 'GET', value: 'GET' },
|
||||
{ label: 'PUT', value: 'PUT' },
|
||||
{ label: 'DELETE', value: 'DELETE' },
|
||||
];
|
||||
|
||||
const title = computed(() => {
|
||||
const map: Record<string, string> = { start: '发起人设置', approver: '审批人设置', cc: '抄送人设置', branch: '条件设置' };
|
||||
return map[node.value?.type || ''] || '节点设置';
|
||||
});
|
||||
|
||||
const roleApi = (params) => defHttp.get({ url: '/sys/role/list', params });
|
||||
|
||||
function openDrawer(n: FlowNode) {
|
||||
node.value = n;
|
||||
// 编辑副本,确定时回写,避免取消后脏数据
|
||||
form.value = { name: n.name, props: cloneDeep(n.props) };
|
||||
// 审批人节点确保回调接口配置结构存在
|
||||
if (n.type === 'approver') {
|
||||
const cb = form.value.props.callbackActions || {};
|
||||
form.value.props.callbackActions = {
|
||||
onNodeApprove: Array.isArray(cb.onNodeApprove) ? cb.onNodeApprove : [],
|
||||
onApprove: Array.isArray(cb.onApprove) ? cb.onApprove : [],
|
||||
onReject: Array.isArray(cb.onReject) ? cb.onReject : [],
|
||||
};
|
||||
}
|
||||
open.value = true;
|
||||
}
|
||||
|
||||
function addAction(phaseKey: string) {
|
||||
form.value.props.callbackActions[phaseKey].push({ name: '', method: 'POST', url: '' });
|
||||
}
|
||||
|
||||
function removeAction(phaseKey: string, i: number) {
|
||||
form.value.props.callbackActions[phaseKey].splice(i, 1);
|
||||
}
|
||||
|
||||
/** 录制:临时隐藏设计器,捕获用户点击业务按钮的请求并回填 */
|
||||
async function recordInto(phaseKey: string) {
|
||||
const cap = await startFlowApiRecord();
|
||||
if (cap) {
|
||||
form.value.props.callbackActions[phaseKey].push({ name: '', method: cap.method, url: cap.url, body: cap.data });
|
||||
createMessage.success(`已录制接口:${cap.method} ${cap.url}`);
|
||||
}
|
||||
}
|
||||
|
||||
function cancelRecord() {
|
||||
cancelFlowApiRecord();
|
||||
}
|
||||
|
||||
function onClose() {
|
||||
open.value = false;
|
||||
}
|
||||
|
||||
function onConfirm() {
|
||||
if (node.value && form.value) {
|
||||
node.value.name = form.value.name;
|
||||
node.value.props = cloneDeep(form.value.props);
|
||||
emit('confirm', node.value);
|
||||
}
|
||||
open.value = false;
|
||||
}
|
||||
|
||||
function addCond() {
|
||||
form.value.props.conditions.push({ label: '', field: '', operator: 'eq', value: '' });
|
||||
}
|
||||
|
||||
function removeCond(i: number) {
|
||||
form.value.props.conditions.splice(i, 1);
|
||||
}
|
||||
|
||||
defineExpose({ openDrawer });
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.fd-cond-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.fd-cond-del {
|
||||
color: #ff4d4f;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* 回调接口配置 */
|
||||
.fd-cb-block {
|
||||
margin-bottom: 14px;
|
||||
padding: 10px;
|
||||
background: #fafafa;
|
||||
border: 1px solid #f0f0f0;
|
||||
border-radius: 6px;
|
||||
}
|
||||
.fd-cb-title {
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: #595959;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.fd-cb-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
.fd-cb-del {
|
||||
color: #ff4d4f;
|
||||
cursor: pointer;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
<!-- 录制提示条 Teleport 到 body,需非 scoped 全局样式 -->
|
||||
<style lang="less">
|
||||
.flow-record-banner {
|
||||
position: fixed;
|
||||
top: 16px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
z-index: 99999;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
max-width: 92vw;
|
||||
padding: 10px 16px;
|
||||
border-radius: 8px;
|
||||
background: #fffbe6;
|
||||
border: 1px solid #ffe58f;
|
||||
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.15);
|
||||
color: #614700;
|
||||
font-size: 13px;
|
||||
|
||||
.flow-record-banner-icon {
|
||||
color: #fa8c16;
|
||||
font-size: 18px;
|
||||
}
|
||||
.flow-record-banner-text {
|
||||
line-height: 1.5;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
428
jeecgboot-vue3/src/views/approval/flow/components/flow.less
Normal file
428
jeecgboot-vue3/src/views/approval/flow/components/flow.less
Normal file
@@ -0,0 +1,428 @@
|
||||
/**
|
||||
* 钉钉式审批流设计器 样式
|
||||
* @author GHT
|
||||
* @date 2026-05-29 for:【QH-MES审批流设计】新增审批流可视化设计
|
||||
*/
|
||||
@line-color: #cacaca;
|
||||
|
||||
/* 录制回调接口时,隐藏整个设计器弹窗(含遮罩),让用户能点击业务页面上的真实按钮 */
|
||||
.flow-design-recording-hide {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.fd-design {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.fd-toolbar {
|
||||
flex-shrink: 0;
|
||||
padding: 10px 16px;
|
||||
background: #fff;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
|
||||
.fd-tb-item {
|
||||
margin-right: 24px;
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.fd-tb-tip {
|
||||
color: #999;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
/* update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】候选阶段侧边栏布局----- */
|
||||
.fd-body {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.fd-palette {
|
||||
flex-shrink: 0;
|
||||
width: 220px;
|
||||
overflow-y: auto;
|
||||
background: #fff;
|
||||
border-right: 1px solid #f0f0f0;
|
||||
padding: 14px 12px;
|
||||
|
||||
.fd-palette-title {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.fd-palette-tip {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
line-height: 1.5;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.fd-palette-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.fd-palette-item {
|
||||
position: relative;
|
||||
border: 1px solid #e8e8e8;
|
||||
border-radius: 6px;
|
||||
padding: 8px 30px 8px 10px;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s;
|
||||
border-left: 3px solid #ff943e;
|
||||
|
||||
&.fd-palette-cc {
|
||||
border-left-color: #3296fa;
|
||||
}
|
||||
|
||||
.fd-palette-item-name {
|
||||
font-size: 13px;
|
||||
color: #333;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.fd-palette-item-field {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
margin-top: 2px;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.fd-palette-item-add {
|
||||
position: absolute;
|
||||
right: 10px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
color: #3296fa;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
border-color: #3296fa;
|
||||
background: #f0f7ff;
|
||||
box-shadow: 0 2px 8px rgba(50, 150, 250, 0.15);
|
||||
}
|
||||
}
|
||||
}
|
||||
/* update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】候选阶段侧边栏布局----- */
|
||||
|
||||
.fd-canvas {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
background: #f0f2f5;
|
||||
padding: 40px 20px 80px;
|
||||
}
|
||||
|
||||
.fd-flow {
|
||||
display: inline-flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
min-width: 100%;
|
||||
}
|
||||
|
||||
/* ---------- 节点列 ---------- */
|
||||
.fd-node {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
flex-grow: 1;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.fd-card-wrap {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.fd-card {
|
||||
position: relative;
|
||||
width: 220px;
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 1px 5px rgba(0, 0, 0, 0.12);
|
||||
cursor: pointer;
|
||||
transition: box-shadow 0.2s;
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.2);
|
||||
|
||||
.fd-del {
|
||||
display: inline-flex;
|
||||
}
|
||||
}
|
||||
|
||||
&.fd-error {
|
||||
box-shadow: 0 0 0 1px #ff4d4f;
|
||||
}
|
||||
}
|
||||
|
||||
.fd-card-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 8px 12px;
|
||||
font-size: 13px;
|
||||
color: #fff;
|
||||
border-radius: 8px 8px 0 0;
|
||||
|
||||
.fd-title {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.fd-priority {
|
||||
font-size: 12px;
|
||||
opacity: 0.85;
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.fd-del {
|
||||
display: none;
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
opacity: 0.85;
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.fd-card-body {
|
||||
padding: 12px;
|
||||
font-size: 13px;
|
||||
color: #555;
|
||||
min-height: 22px;
|
||||
line-height: 1.5;
|
||||
word-break: break-all;
|
||||
|
||||
&.fd-placeholder {
|
||||
color: #f56c6c;
|
||||
}
|
||||
}
|
||||
|
||||
/* 各类型节点头部配色 */
|
||||
.fd-start .fd-card-header {
|
||||
background: #576a95;
|
||||
}
|
||||
.fd-approver .fd-card-header {
|
||||
background: #ff943e;
|
||||
}
|
||||
.fd-cc .fd-card-header {
|
||||
background: #3296fa;
|
||||
}
|
||||
.fd-branch {
|
||||
.fd-card {
|
||||
width: 220px;
|
||||
}
|
||||
.fd-card-header {
|
||||
background: #fff;
|
||||
color: #15bca3;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
|
||||
.fd-priority {
|
||||
color: #999;
|
||||
}
|
||||
.fd-del {
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* ---------- 节点之间的连线 + 加号 ---------- */
|
||||
.fd-add {
|
||||
position: relative;
|
||||
width: 2px;
|
||||
min-height: 70px;
|
||||
background: @line-color;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.fd-add-btn {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
border: none;
|
||||
border-radius: 50%;
|
||||
background: #3296fa;
|
||||
color: #fff;
|
||||
font-size: 16px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: 0 2px 6px rgba(50, 150, 250, 0.35);
|
||||
transition: transform 0.15s;
|
||||
|
||||
&:hover {
|
||||
transform: scale(1.12);
|
||||
}
|
||||
}
|
||||
|
||||
/* 加号弹出菜单 */
|
||||
.fd-add-menu {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
padding: 4px;
|
||||
}
|
||||
.fd-add-item {
|
||||
width: 76px;
|
||||
height: 70px;
|
||||
border: 1px solid #f0f0f0;
|
||||
border-radius: 6px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 6px;
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
color: #333;
|
||||
transition: all 0.15s;
|
||||
|
||||
.anticon {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
border-color: #3296fa;
|
||||
color: #3296fa;
|
||||
background: #f0f7ff;
|
||||
}
|
||||
|
||||
&.fd-add-approver .anticon {
|
||||
color: #ff943e;
|
||||
}
|
||||
&.fd-add-cc .anticon {
|
||||
color: #3296fa;
|
||||
}
|
||||
&.fd-add-condition .anticon {
|
||||
color: #15bca3;
|
||||
}
|
||||
}
|
||||
|
||||
/* ---------- 条件分支 ---------- */
|
||||
.fd-branches {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* +条件 按钮(带进入竖线) */
|
||||
.fd-add-branch-wrap {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 46px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 2px;
|
||||
height: 100%;
|
||||
background: @line-color;
|
||||
}
|
||||
|
||||
.fd-add-branch {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
border-radius: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.fd-branch-cols {
|
||||
display: flex;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.fd-branch-col {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
background: #f0f2f5;
|
||||
border-top: 2px solid @line-color;
|
||||
border-bottom: 2px solid @line-color;
|
||||
padding: 0 24px;
|
||||
|
||||
/* 列内顶部进入竖线 + 底部汇出竖线 */
|
||||
.fd-branch-inner {
|
||||
position: relative;
|
||||
padding-top: 30px;
|
||||
padding-bottom: 0;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 2px;
|
||||
height: 30px;
|
||||
background: @line-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 首末列外侧白块,遮挡多余横线,形成包裹效果 */
|
||||
.fd-cover {
|
||||
position: absolute;
|
||||
width: 50%;
|
||||
height: 4px;
|
||||
background: #f0f2f5;
|
||||
z-index: 2;
|
||||
}
|
||||
.fd-cover-tl {
|
||||
top: -2px;
|
||||
left: -1px;
|
||||
}
|
||||
.fd-cover-bl {
|
||||
bottom: -2px;
|
||||
left: -1px;
|
||||
}
|
||||
.fd-cover-tr {
|
||||
top: -2px;
|
||||
right: -1px;
|
||||
}
|
||||
.fd-cover-br {
|
||||
bottom: -2px;
|
||||
right: -1px;
|
||||
}
|
||||
|
||||
/* ---------- 结束节点 ---------- */
|
||||
.fd-end {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding-top: 0;
|
||||
|
||||
.fd-end-dot {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 50%;
|
||||
background: #dedede;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
span {
|
||||
font-size: 13px;
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
267
jeecgboot-vue3/src/views/approval/flow/components/flowTypes.ts
Normal file
267
jeecgboot-vue3/src/views/approval/flow/components/flowTypes.ts
Normal file
@@ -0,0 +1,267 @@
|
||||
/**
|
||||
* 钉钉式审批流 节点数据模型 + 工厂函数
|
||||
* @author GHT
|
||||
* @date 2026-05-29 for:【QH-MES审批流设计】新增审批流可视化设计
|
||||
*/
|
||||
|
||||
// 节点类型:start发起人 approver审批人 cc抄送人 condition条件路由 branch条件分支
|
||||
export type NodeType = 'start' | 'approver' | 'cc' | 'condition' | 'branch';
|
||||
|
||||
export interface FlowNode {
|
||||
id: string;
|
||||
type: NodeType;
|
||||
name: string;
|
||||
// 节点配置(按 type 不同含义不同)
|
||||
props: Record<string, any>;
|
||||
// 链式下一个节点
|
||||
childNode?: FlowNode | null;
|
||||
// 仅 condition 节点:分支数组(每个元素 type=branch)
|
||||
conditionNodes?: FlowNode[];
|
||||
}
|
||||
|
||||
let _seed = 0;
|
||||
|
||||
/** 生成唯一节点 id */
|
||||
export function nid(prefix = 'node'): string {
|
||||
_seed += 1;
|
||||
return `${prefix}_${Date.now().toString(36)}_${_seed}`;
|
||||
}
|
||||
|
||||
/** 发起人节点(根节点,固定存在) */
|
||||
export function createStartNode(): FlowNode {
|
||||
return {
|
||||
id: nid('start'),
|
||||
type: 'start',
|
||||
name: '发起人',
|
||||
props: {
|
||||
// initiatorType: all全员 / user指定成员 / role指定角色
|
||||
initiatorType: 'all',
|
||||
userText: '',
|
||||
roleList: [],
|
||||
},
|
||||
childNode: null,
|
||||
};
|
||||
}
|
||||
|
||||
/** 审批人节点 */
|
||||
export function createApproverNode(): FlowNode {
|
||||
return {
|
||||
id: nid('approver'),
|
||||
type: 'approver',
|
||||
name: '审批人',
|
||||
props: {
|
||||
// approverType: user指定成员 / role指定角色 / leader主管 / self发起人自己
|
||||
approverType: 'user',
|
||||
userText: '',
|
||||
roleList: [],
|
||||
leaderLevel: 1,
|
||||
// 多人审批方式 and会签(需全部同意) / or或签(一人同意) / sequence依次审批
|
||||
multiMode: 'and',
|
||||
// 审批人为空时 pass自动通过 / admin转交管理员 / stop终止
|
||||
emptyStrategy: 'admin',
|
||||
},
|
||||
childNode: null,
|
||||
};
|
||||
}
|
||||
|
||||
/** 抄送人节点 */
|
||||
export function createCcNode(): FlowNode {
|
||||
return {
|
||||
id: nid('cc'),
|
||||
type: 'cc',
|
||||
name: '抄送人',
|
||||
props: {
|
||||
// ccType: user指定成员/角色 / field取单据字段中的人员
|
||||
ccType: 'user',
|
||||
userText: '',
|
||||
roleList: [],
|
||||
allowEditCc: false,
|
||||
},
|
||||
childNode: null,
|
||||
};
|
||||
}
|
||||
|
||||
// update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】按当前页解析的字段生成审批阶段节点-----
|
||||
/** 解析出的页面阶段字段 */
|
||||
export interface StageField {
|
||||
stageKey: string;
|
||||
stageName: string;
|
||||
nodeType: 'approver' | 'cc';
|
||||
field: string;
|
||||
fieldComment?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 由"当前页字段"生成阶段节点:
|
||||
* 审批阶段(校对/审核/审批/分发) -> 审批人节点,处理人=取单据该字段中的人员;
|
||||
* 抄送阶段 -> 抄送节点,抄送人=取单据该字段中的人员。
|
||||
*/
|
||||
export function createStageNode(stage: StageField): FlowNode {
|
||||
const fieldLabel = stage.fieldComment || stage.field;
|
||||
if (stage.nodeType === 'cc') {
|
||||
const node = createCcNode();
|
||||
node.name = stage.stageName;
|
||||
node.props.ccType = 'field';
|
||||
node.props.fieldName = stage.field;
|
||||
node.props.fieldLabel = fieldLabel;
|
||||
return node;
|
||||
}
|
||||
const node = createApproverNode();
|
||||
node.name = stage.stageName;
|
||||
node.props.approverType = 'field';
|
||||
node.props.fieldName = stage.field;
|
||||
node.props.fieldLabel = fieldLabel;
|
||||
return node;
|
||||
}
|
||||
// update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】按当前页解析的字段生成审批阶段节点-----
|
||||
|
||||
/** 条件分支节点(默认两条分支:条件 + 其它情况) */
|
||||
export function createConditionNode(): FlowNode {
|
||||
return {
|
||||
id: nid('condition'),
|
||||
type: 'condition',
|
||||
name: '条件分支',
|
||||
props: {},
|
||||
conditionNodes: [createBranchNode(1), createBranchNode(2, true)],
|
||||
childNode: null,
|
||||
};
|
||||
}
|
||||
|
||||
/** 单个条件分支 */
|
||||
export function createBranchNode(priority: number, isDefault = false): FlowNode {
|
||||
return {
|
||||
id: nid('branch'),
|
||||
type: 'branch',
|
||||
name: isDefault ? '其它情况' : `条件${priority}`,
|
||||
props: {
|
||||
priorityLevel: priority,
|
||||
isDefault,
|
||||
// 条件列表:{ field 字段名, label 字段中文, operator 运算符, value 值 }
|
||||
conditions: [] as any[],
|
||||
// 多条件关系 and / or
|
||||
logic: 'and',
|
||||
},
|
||||
childNode: null,
|
||||
};
|
||||
}
|
||||
|
||||
/** 运算符选项 */
|
||||
export const OPERATOR_OPTIONS = [
|
||||
{ label: '等于', value: 'eq' },
|
||||
{ label: '不等于', value: 'ne' },
|
||||
{ label: '大于', value: 'gt' },
|
||||
{ label: '大于等于', value: 'gte' },
|
||||
{ label: '小于', value: 'lt' },
|
||||
{ label: '小于等于', value: 'lte' },
|
||||
{ label: '包含', value: 'contains' },
|
||||
{ label: '为空', value: 'empty' },
|
||||
{ label: '不为空', value: 'notEmpty' },
|
||||
];
|
||||
|
||||
/** 节点卡片内容摘要文本 */
|
||||
export function nodeSummary(node: FlowNode): string {
|
||||
if (node.type === 'start') {
|
||||
const t = node.props.initiatorType;
|
||||
if (t === 'all') return '所有人可发起';
|
||||
if (t === 'user') return node.props.userText ? `指定成员:${node.props.userText}` : '请设置发起人';
|
||||
if (t === 'role') return node.props.roleList?.length ? `指定角色(${node.props.roleList.length})` : '请设置发起角色';
|
||||
return '请设置发起人';
|
||||
}
|
||||
if (node.type === 'approver') {
|
||||
const t = node.props.approverType;
|
||||
if (t === 'self') return '发起人自己';
|
||||
if (t === 'leader') return `第${node.props.leaderLevel || 1}级主管`;
|
||||
if (t === 'role') return node.props.roleList?.length ? `角色审批(${node.props.roleList.length})` : '请设置审批角色';
|
||||
// update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】取单据字段审批人摘要-----
|
||||
if (t === 'field') return `取单据字段:${node.props.fieldLabel || node.props.fieldName || '未指定字段'}`;
|
||||
// update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】取单据字段审批人摘要-----
|
||||
return node.props.userText ? `指定成员:${node.props.userText}` : '请设置审批人';
|
||||
}
|
||||
if (node.type === 'cc') {
|
||||
// update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】取单据字段抄送人摘要-----
|
||||
if (node.props.ccType === 'field') return `抄送单据字段:${node.props.fieldLabel || node.props.fieldName || '未指定字段'}`;
|
||||
// update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】取单据字段抄送人摘要-----
|
||||
return node.props.userText ? `抄送:${node.props.userText}` : '请设置抄送人';
|
||||
}
|
||||
if (node.type === 'branch') {
|
||||
if (node.props.isDefault) return '未满足其它条件时进入此分支';
|
||||
const list = node.props.conditions || [];
|
||||
if (!list.length) return '请设置条件';
|
||||
return list.map((c: any) => `${c.label || c.field} ${operatorText(c.operator)} ${c.value ?? ''}`).join(node.props.logic === 'or' ? ' 或 ' : ' 且 ');
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
function operatorText(op: string): string {
|
||||
return OPERATOR_OPTIONS.find((o) => o.value === op)?.label || op;
|
||||
}
|
||||
|
||||
/** 深度遍历每个节点(含分支) */
|
||||
export function eachNode(node: FlowNode | null | undefined, cb: (n: FlowNode) => void) {
|
||||
if (!node) return;
|
||||
cb(node);
|
||||
if (node.childNode) eachNode(node.childNode, cb);
|
||||
if (node.conditionNodes) node.conditionNodes.forEach((b) => eachNode(b, cb));
|
||||
}
|
||||
|
||||
/** 在 prevId 节点之后插入新节点 */
|
||||
export function insertAfter(root: FlowNode, prevId: string, newNode: FlowNode) {
|
||||
eachNode(root, (n) => {
|
||||
if (n.id === prevId && newNode.id !== prevId) {
|
||||
newNode.childNode = n.childNode || null;
|
||||
n.childNode = newNode;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/** 删除节点(普通链节点 / 条件分支,发起人不可删) */
|
||||
export function removeNode(root: FlowNode, id: string): boolean {
|
||||
// 1) 链式节点:找到以其为 childNode 的父节点
|
||||
let parent: FlowNode | null = null;
|
||||
eachNode(root, (n) => {
|
||||
if (n.childNode && n.childNode.id === id) parent = n;
|
||||
});
|
||||
if (parent) {
|
||||
(parent as FlowNode).childNode = (parent as FlowNode).childNode?.childNode || null;
|
||||
return true;
|
||||
}
|
||||
// 2) 条件分支:找到包含该分支的 condition
|
||||
let cond: FlowNode | null = null;
|
||||
eachNode(root, (n) => {
|
||||
if (n.conditionNodes && n.conditionNodes.some((b) => b.id === id)) cond = n;
|
||||
});
|
||||
if (cond) {
|
||||
const c = cond as FlowNode;
|
||||
const arr = c.conditionNodes as FlowNode[];
|
||||
if (arr.length <= 2) {
|
||||
// 只剩两条分支时删除一条 => 整个条件节点收起,保留其后续链
|
||||
let condParent: FlowNode | null = null;
|
||||
eachNode(root, (n) => {
|
||||
if (n.childNode && n.childNode.id === c.id) condParent = n;
|
||||
});
|
||||
if (condParent) (condParent as FlowNode).childNode = c.childNode || null;
|
||||
} else {
|
||||
const i = arr.findIndex((b) => b.id === id);
|
||||
if (i >= 0) arr.splice(i, 1);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/** 给条件节点新增一条分支 */
|
||||
export function addBranch(root: FlowNode, conditionId: string) {
|
||||
eachNode(root, (n) => {
|
||||
if (n.id === conditionId && n.conditionNodes) {
|
||||
const next = n.conditionNodes.length + 1;
|
||||
// 新分支插入到"其它情况"默认分支之前
|
||||
const defaultIdx = n.conditionNodes.findIndex((b) => b.props.isDefault);
|
||||
const branch = createBranchNode(next);
|
||||
if (defaultIdx >= 0) {
|
||||
n.conditionNodes.splice(defaultIdx, 0, branch);
|
||||
} else {
|
||||
n.conditionNodes.push(branch);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
34
jeecgboot-vue3/src/views/approval/flow/launch.api.ts
Normal file
34
jeecgboot-vue3/src/views/approval/flow/launch.api.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import { defHttp } from '/@/utils/http/axios';
|
||||
|
||||
/**
|
||||
* 发起审批 API(全局悬浮按钮使用)
|
||||
* @author GHT
|
||||
* @date 2026-05-29 for:【QH-MES审批流设计】发起审批运行时
|
||||
*/
|
||||
enum Api {
|
||||
publishedList = '/xslmes/approvalLaunch/publishedList',
|
||||
bizRecords = '/xslmes/approvalLaunch/bizRecords',
|
||||
launch = '/xslmes/approvalLaunch/launch',
|
||||
launchBatch = '/xslmes/approvalLaunch/launchBatch',
|
||||
}
|
||||
|
||||
/**
|
||||
* 已发布审批流列表(可发起的单据类型)
|
||||
*/
|
||||
export const getPublishedFlows = () => defHttp.get({ url: Api.publishedList });
|
||||
|
||||
/**
|
||||
* 根据审批流查询其绑定单据的记录列表
|
||||
*/
|
||||
export const getBizRecords = (params: { flowId: string; keyword?: string }) => defHttp.get({ url: Api.bizRecords, params });
|
||||
|
||||
/**
|
||||
* 发起审批(单条)
|
||||
*/
|
||||
export const launchApproval = (params: { flowId: string; bizDataId: string; bizTitle?: string }) => defHttp.post({ url: Api.launch, params });
|
||||
|
||||
/**
|
||||
* 批量发起审批(列表多选)
|
||||
*/
|
||||
export const launchApprovalBatch = (params: { flowId: string; items: { bizDataId: string; bizTitle?: string }[] }) =>
|
||||
defHttp.post({ url: Api.launchBatch, params });
|
||||
154
jeecgboot-vue3/src/views/system/im/ImApprovalDetailModal.vue
Normal file
154
jeecgboot-vue3/src/views/system/im/ImApprovalDetailModal.vue
Normal file
@@ -0,0 +1,154 @@
|
||||
<!--
|
||||
IM 审批卡片「查看详情」弹窗:展示单据全部字段 + 审批进度/历史
|
||||
@author GHT
|
||||
@date 2026-05-29 for:【QH-MES审批流设计】审批办理-查看详情
|
||||
-->
|
||||
<template>
|
||||
<a-modal v-model:open="open" title="审批单据详情" :width="640" :footer="null" :destroyOnClose="true">
|
||||
<a-spin :spinning="loading">
|
||||
<div class="im-appr-detail">
|
||||
<div class="im-appr-detail-head">
|
||||
<a-descriptions :column="2" size="small" bordered>
|
||||
<a-descriptions-item label="审批流">{{ info.flowName }}</a-descriptions-item>
|
||||
<a-descriptions-item label="状态">
|
||||
<a-tag :color="statusColor">{{ info.statusText }}</a-tag>
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="发起人">{{ info.applyUserName }}</a-descriptions-item>
|
||||
<a-descriptions-item label="当前节点">{{ info.currentNodeName || '-' }}</a-descriptions-item>
|
||||
<a-descriptions-item label="当前处理人" :span="2">{{ info.currentHandlersText || '-' }}</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
</div>
|
||||
|
||||
<div class="im-appr-detail-section-title">单据数据</div>
|
||||
<table class="im-appr-detail-table">
|
||||
<tbody>
|
||||
<tr v-for="f in fields" :key="f.label">
|
||||
<th>{{ f.label }}</th>
|
||||
<td>{{ f.value }}</td>
|
||||
</tr>
|
||||
<tr v-if="!fields.length">
|
||||
<td class="im-appr-detail-empty">暂无单据数据</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<template v-if="history.length">
|
||||
<div class="im-appr-detail-section-title">审批历史</div>
|
||||
<a-timeline class="im-appr-detail-timeline">
|
||||
<a-timeline-item v-for="(h, i) in history" :key="i" :color="h.actionText === '驳回' ? 'red' : 'green'">
|
||||
<div class="im-appr-his-line">
|
||||
<b>{{ h.nodeName }}</b>
|
||||
<span>{{ h.name }} {{ h.actionText }}</span>
|
||||
<span class="im-appr-his-time">{{ h.time }}</span>
|
||||
</div>
|
||||
<div v-if="h.comment" class="im-appr-his-comment">意见:{{ h.comment }}</div>
|
||||
</a-timeline-item>
|
||||
</a-timeline>
|
||||
</template>
|
||||
</div>
|
||||
</a-spin>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, ref } from 'vue';
|
||||
import { useMessage } from '/@/hooks/web/useMessage';
|
||||
import { getApprovalDetail } from '/@/views/approval/flow/approvalHandle.api';
|
||||
|
||||
defineOptions({ name: 'ImApprovalDetailModal' });
|
||||
|
||||
const { createMessage } = useMessage();
|
||||
|
||||
const open = ref(false);
|
||||
const loading = ref(false);
|
||||
const info = ref<any>({});
|
||||
const fields = ref<{ label: string; value: string }[]>([]);
|
||||
const history = ref<any[]>([]);
|
||||
|
||||
const statusColor = computed(() => {
|
||||
const s = info.value?.status;
|
||||
if (s === '1') return 'green';
|
||||
if (s === '2') return 'red';
|
||||
if (s === '3') return 'default';
|
||||
return 'blue';
|
||||
});
|
||||
|
||||
async function openModal(instanceId: string) {
|
||||
open.value = true;
|
||||
info.value = {};
|
||||
fields.value = [];
|
||||
history.value = [];
|
||||
if (!instanceId) return;
|
||||
try {
|
||||
loading.value = true;
|
||||
const data: any = await getApprovalDetail(instanceId);
|
||||
info.value = data || {};
|
||||
fields.value = data?.fields || [];
|
||||
history.value = data?.history || [];
|
||||
} catch (e: any) {
|
||||
createMessage.error(e?.message || '获取详情失败');
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
defineExpose({ openModal });
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.im-appr-detail-section-title {
|
||||
margin: 16px 0 8px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
border-left: 3px solid #1677ff;
|
||||
padding-left: 8px;
|
||||
}
|
||||
|
||||
.im-appr-detail-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 13px;
|
||||
border: 1px solid #f0f0f0;
|
||||
|
||||
th,
|
||||
td {
|
||||
padding: 8px 10px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
text-align: left;
|
||||
vertical-align: top;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
th {
|
||||
width: 32%;
|
||||
background: #fafafa;
|
||||
color: #595959;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.im-appr-detail-empty {
|
||||
color: #999;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
.im-appr-his-line {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
font-size: 13px;
|
||||
|
||||
.im-appr-his-time {
|
||||
color: #999;
|
||||
font-size: 12px;
|
||||
margin-left: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.im-appr-his-comment {
|
||||
margin-top: 2px;
|
||||
font-size: 12px;
|
||||
color: #888;
|
||||
}
|
||||
</style>
|
||||
@@ -1,442 +1,426 @@
|
||||
<template>
|
||||
|
||||
<div class="im-biz-record-message">
|
||||
|
||||
<div v-if="showNoPermission" class="im-biz-record-no-permission">暂无当前消息权限</div>
|
||||
|
||||
<template v-else>
|
||||
<!-- 单条:详情表 -->
|
||||
<template v-if="isSingleItem">
|
||||
<div class="im-biz-record-item">
|
||||
<div class="im-biz-record-table-wrap">
|
||||
<table class="im-biz-record-table im-biz-record-table--detail">
|
||||
<tbody>
|
||||
<tr v-for="field in resolveItemFields(singleItem)" :key="field.label">
|
||||
<th>{{ field.label }}</th>
|
||||
<td>{{ field.value }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<template v-if="isSingleItem">
|
||||
|
||||
<div class="im-biz-record-item">
|
||||
|
||||
<div class="im-biz-record-table-wrap">
|
||||
|
||||
<table class="im-biz-record-table im-biz-record-table--detail">
|
||||
|
||||
<tbody>
|
||||
|
||||
<tr v-for="field in resolveItemFields(singleItem)" :key="field.label">
|
||||
|
||||
<th>{{ field.label }}</th>
|
||||
|
||||
<td>{{ field.value }}</td>
|
||||
|
||||
</tr>
|
||||
|
||||
</tbody>
|
||||
|
||||
</table>
|
||||
<!-- 审批卡片:底部操作按钮 -->
|
||||
<div v-if="isApprovalCard" class="im-biz-record-actions">
|
||||
<a-button size="small" @click="handleDetail(singleItem)">
|
||||
<Icon icon="ant-design:file-search-outlined" />
|
||||
<span>查看详情</span>
|
||||
</a-button>
|
||||
<template v-if="liveActionable">
|
||||
<a-button size="small" type="primary" :loading="approving" @click="handleApprove(singleItem)">
|
||||
<Icon icon="ant-design:check-outlined" />
|
||||
<span>{{ singleItem.actionLabel || '审批' }}</span>
|
||||
</a-button>
|
||||
<a-button size="small" danger @click="openReject(singleItem)">
|
||||
<Icon icon="ant-design:close-outlined" />
|
||||
<span>拒绝</span>
|
||||
</a-button>
|
||||
</template>
|
||||
<!-- 不可办理(已处理/已流转/非当前处理人):置灰提示 -->
|
||||
<span v-else-if="!props.mine && disabledText" class="im-biz-record-disabled">{{ disabledText }}</span>
|
||||
<a v-if="canLocate" class="im-biz-record-link" @click.prevent="handleLinkClick(singleItem.linkPath)">
|
||||
<Icon icon="ant-design:unordered-list-outlined" />
|
||||
<span>跳转至列表</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- 普通分享卡片:定位链接 -->
|
||||
<a v-else class="im-biz-record-link" @click.prevent="handleLinkClick(singleItem.linkPath)">
|
||||
<Icon icon="ant-design:link-outlined" />
|
||||
<span>查看并定位到此数据</span>
|
||||
</a>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<a class="im-biz-record-link" @click.prevent="handleLinkClick(singleItem.linkPath)">
|
||||
|
||||
<Icon icon="ant-design:link-outlined" />
|
||||
|
||||
<span>查看并定位到此数据</span>
|
||||
|
||||
</a>
|
||||
|
||||
</div>
|
||||
<!-- 多条:列表表,第一列为定位链接 -->
|
||||
<template v-else>
|
||||
<div class="im-biz-record-table-wrap im-biz-record-table-wrap--list">
|
||||
<table class="im-biz-record-table im-biz-record-table--list">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="im-biz-record-link-col">链接</th>
|
||||
<th v-for="columnLabel in listColumnLabels" :key="columnLabel">{{ columnLabel }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="(item, index) in payload.items" :key="item.recordId || index">
|
||||
<td class="im-biz-record-link-col">
|
||||
<a class="im-biz-record-link" @click.prevent="handleLinkClick(item.linkPath)">
|
||||
<Icon icon="ant-design:link-outlined" />
|
||||
<span>定位</span>
|
||||
</a>
|
||||
</td>
|
||||
<td v-for="columnLabel in listColumnLabels" :key="columnLabel">
|
||||
{{ getFieldValue(item, columnLabel) }}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div v-if="showPeerNoPermissionTip" class="im-biz-record-peer-tip">对方无此功能权限</div>
|
||||
</template>
|
||||
|
||||
<!-- 查看详情弹窗 -->
|
||||
<ImApprovalDetailModal ref="detailModalRef" />
|
||||
|
||||
|
||||
<!-- 多条:列表表,第一列为定位链接 -->
|
||||
|
||||
<template v-else>
|
||||
|
||||
<div class="im-biz-record-table-wrap im-biz-record-table-wrap--list">
|
||||
|
||||
<table class="im-biz-record-table im-biz-record-table--list">
|
||||
|
||||
<thead>
|
||||
|
||||
<tr>
|
||||
|
||||
<th class="im-biz-record-link-col">链接</th>
|
||||
|
||||
<th v-for="columnLabel in listColumnLabels" :key="columnLabel">{{ columnLabel }}</th>
|
||||
|
||||
</tr>
|
||||
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
|
||||
<tr v-for="(item, index) in payload.items" :key="item.recordId || index">
|
||||
|
||||
<td class="im-biz-record-link-col">
|
||||
|
||||
<a class="im-biz-record-link" @click.prevent="handleLinkClick(item.linkPath)">
|
||||
|
||||
<Icon icon="ant-design:link-outlined" />
|
||||
|
||||
<span>定位</span>
|
||||
|
||||
</a>
|
||||
|
||||
</td>
|
||||
|
||||
<td v-for="columnLabel in listColumnLabels" :key="columnLabel">
|
||||
|
||||
{{ getFieldValue(item, columnLabel) }}
|
||||
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
|
||||
</tbody>
|
||||
|
||||
</table>
|
||||
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
<div v-if="showPeerNoPermissionTip" class="im-biz-record-peer-tip">对方无此功能权限</div>
|
||||
|
||||
</template>
|
||||
|
||||
<!-- 驳回理由弹窗 -->
|
||||
<a-modal v-model:open="rejectOpen" title="驳回审批" :confirmLoading="rejecting" okText="确认驳回" @ok="confirmReject">
|
||||
<a-form layout="vertical">
|
||||
<a-form-item label="驳回理由" required>
|
||||
<a-textarea v-model:value="rejectReason" :rows="3" placeholder="请填写驳回理由" :maxlength="500" show-count />
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
|
||||
|
||||
<script lang="ts" setup>
|
||||
|
||||
import { computed } from 'vue';
|
||||
|
||||
import { computed, ref, onMounted, watch } from 'vue';
|
||||
import type { ImBizRecordItem, ImBizRecordPayload } from './imBizRecordMessage';
|
||||
|
||||
import {
|
||||
|
||||
getImBizRecordFieldValueByLabel,
|
||||
|
||||
resolveImBizRecordItemFields,
|
||||
|
||||
resolveImBizRecordListColumnLabels,
|
||||
|
||||
} from './imBizRecordMessage';
|
||||
|
||||
import { getImBizRecordFieldValueByLabel, resolveImBizRecordItemFields, resolveImBizRecordListColumnLabels } from './imBizRecordMessage';
|
||||
import { navigateImBizRecordLink } from './imRecordLocate';
|
||||
|
||||
import { hasImBizRecordPagePermission } from './imBizRecordPermission';
|
||||
|
||||
|
||||
import { useMessage } from '/@/hooks/web/useMessage';
|
||||
import { approveApproval, rejectApproval, getApprovalStatus } from '/@/views/approval/flow/approvalHandle.api';
|
||||
import ImApprovalDetailModal from './ImApprovalDetailModal.vue';
|
||||
|
||||
defineOptions({ name: 'ImBizRecordMessageContent' });
|
||||
|
||||
|
||||
|
||||
const props = defineProps<{
|
||||
|
||||
payload: ImBizRecordPayload;
|
||||
|
||||
mine?: boolean;
|
||||
|
||||
receiverHasBizPagePermission?: boolean;
|
||||
|
||||
}>();
|
||||
|
||||
// 办理成功后通知父级(ImChat)刷新当前会话,使下一节点卡片/结果通知即时出现
|
||||
const emit = defineEmits(['handled']);
|
||||
|
||||
const { createMessage } = useMessage();
|
||||
|
||||
const detailModalRef = ref();
|
||||
const approving = ref(false);
|
||||
const rejecting = ref(false);
|
||||
const rejectOpen = ref(false);
|
||||
const rejectReason = ref('');
|
||||
const rejectItem = ref<ImBizRecordItem | null>(null);
|
||||
// 本地办理结果:approved / rejected(卡片消息为静态,办理后本地标记)
|
||||
const actionDone = ref<'' | 'approved' | 'rejected'>('');
|
||||
|
||||
// 审批实例实时状态(用于旧节点卡片置灰)
|
||||
interface LiveStatus {
|
||||
exists: boolean;
|
||||
status?: string;
|
||||
statusText?: string;
|
||||
currentNodeId?: string;
|
||||
currentHandlersText?: string;
|
||||
canApprove?: boolean;
|
||||
}
|
||||
const liveStatus = ref<LiveStatus | null>(null);
|
||||
const liveLoaded = ref(false);
|
||||
|
||||
const isSingleItem = computed(() => props.payload.items.length === 1);
|
||||
|
||||
const singleItem = computed(() => props.payload.items[0]);
|
||||
|
||||
const listColumnLabels = computed(() => resolveImBizRecordListColumnLabels(props.payload.items));
|
||||
|
||||
// 审批卡片:单条且带审批实例ID
|
||||
const isApprovalCard = computed(() => isSingleItem.value && !!singleItem.value?.instanceId);
|
||||
|
||||
// 是否仍可办理:本地未办理 且 实例审批中 且 卡片节点==当前节点 且 本人为当前处理人
|
||||
const liveActionable = computed(() => {
|
||||
if (!isApprovalCard.value || actionDone.value || props.mine) {
|
||||
return false;
|
||||
}
|
||||
const s = liveStatus.value;
|
||||
if (!s || !s.exists || s.status !== '0' || !s.canApprove) {
|
||||
return false;
|
||||
}
|
||||
// 携带 nodeId 时严格比对当前节点,区分同一实例的新旧卡片
|
||||
const cardNodeId = singleItem.value?.nodeId;
|
||||
if (cardNodeId) {
|
||||
return s.currentNodeId === cardNodeId;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
// 不可办理时的置灰提示文案
|
||||
const disabledText = computed(() => {
|
||||
if (actionDone.value) {
|
||||
return actionDone.value === 'rejected' ? '已驳回' : '已处理';
|
||||
}
|
||||
const s = liveStatus.value;
|
||||
if (!s) {
|
||||
return liveLoaded.value ? '加载失败' : '';
|
||||
}
|
||||
if (!s.exists) {
|
||||
return '审批已失效';
|
||||
}
|
||||
if (s.status === '1') return '已通过';
|
||||
if (s.status === '2') return '已驳回';
|
||||
if (s.status === '3') return '已撤销';
|
||||
// 审批中但本卡片不可办理
|
||||
const cardNodeId = singleItem.value?.nodeId;
|
||||
if (cardNodeId && s.currentNodeId !== cardNodeId) {
|
||||
return '已流转,无需处理';
|
||||
}
|
||||
return '等待他人处理';
|
||||
});
|
||||
|
||||
async function loadLiveStatus() {
|
||||
const id = singleItem.value?.instanceId;
|
||||
if (!isApprovalCard.value || props.mine || !id) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const res: any = await getApprovalStatus(id);
|
||||
liveStatus.value = (res || { exists: false }) as LiveStatus;
|
||||
} catch {
|
||||
liveStatus.value = null;
|
||||
} finally {
|
||||
liveLoaded.value = true;
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(loadLiveStatus);
|
||||
watch(() => singleItem.value?.instanceId, loadLiveStatus);
|
||||
|
||||
const hasPagePermission = computed(() => hasImBizRecordPagePermission(props.payload.pagePath));
|
||||
// 审批卡片即使无列表页权限也应可办理/查看详情,仅"跳转至列表"受页面权限约束
|
||||
const showNoPermission = computed(() => !props.mine && !hasPagePermission.value && !isApprovalCard.value);
|
||||
const canLocate = computed(() => props.mine || hasPagePermission.value);
|
||||
|
||||
const showNoPermission = computed(() => !props.mine && !hasPagePermission.value);
|
||||
|
||||
const showPeerNoPermissionTip = computed(
|
||||
() => !!props.mine && props.receiverHasBizPagePermission === false,
|
||||
);
|
||||
const showPeerNoPermissionTip = computed(() => !!props.mine && props.receiverHasBizPagePermission === false);
|
||||
|
||||
function resolveItemFields(item: ImBizRecordItem) {
|
||||
|
||||
return resolveImBizRecordItemFields(item);
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
function getFieldValue(item: ImBizRecordItem, label: string) {
|
||||
|
||||
return getImBizRecordFieldValueByLabel(item, label);
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
async function handleLinkClick(linkPath: string) {
|
||||
|
||||
if (!linkPath || showNoPermission.value) {
|
||||
|
||||
return;
|
||||
|
||||
}
|
||||
|
||||
await navigateImBizRecordLink(linkPath);
|
||||
|
||||
}
|
||||
|
||||
function handleDetail(item: ImBizRecordItem) {
|
||||
if (!item.instanceId) return;
|
||||
detailModalRef.value?.openModal(item.instanceId);
|
||||
}
|
||||
|
||||
async function handleApprove(item: ImBizRecordItem) {
|
||||
if (!item.instanceId || approving.value) return;
|
||||
try {
|
||||
approving.value = true;
|
||||
const res: any = await approveApproval({ instanceId: item.instanceId });
|
||||
createMessage.success(typeof res === 'string' ? res : '已审批');
|
||||
actionDone.value = 'approved';
|
||||
// 立即刷新本卡片状态(置灰),再通知父级刷新会话
|
||||
await loadLiveStatus();
|
||||
emit('handled');
|
||||
} finally {
|
||||
approving.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function openReject(item: ImBizRecordItem) {
|
||||
rejectItem.value = item;
|
||||
rejectReason.value = '';
|
||||
rejectOpen.value = true;
|
||||
}
|
||||
|
||||
async function confirmReject() {
|
||||
const item = rejectItem.value;
|
||||
if (!item?.instanceId) return;
|
||||
if (!rejectReason.value.trim()) {
|
||||
createMessage.warning('请填写驳回理由');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
rejecting.value = true;
|
||||
await rejectApproval({ instanceId: item.instanceId, reason: rejectReason.value.trim() });
|
||||
createMessage.success('已驳回');
|
||||
actionDone.value = 'rejected';
|
||||
rejectOpen.value = false;
|
||||
await loadLiveStatus();
|
||||
emit('handled');
|
||||
} finally {
|
||||
rejecting.value = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
|
||||
<style lang="less" scoped>
|
||||
|
||||
.im-biz-record-message {
|
||||
|
||||
display: flex;
|
||||
|
||||
flex-direction: column;
|
||||
|
||||
gap: 12px;
|
||||
|
||||
min-width: 280px;
|
||||
|
||||
max-width: 420px;
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
.im-biz-record-no-permission {
|
||||
|
||||
padding: 12px 10px;
|
||||
|
||||
font-size: 13px;
|
||||
|
||||
line-height: 1.5;
|
||||
|
||||
color: #8c8c8c;
|
||||
|
||||
text-align: center;
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
.im-biz-record-peer-tip {
|
||||
|
||||
display: inline-flex;
|
||||
|
||||
align-items: center;
|
||||
|
||||
align-self: flex-start;
|
||||
|
||||
margin-top: 4px;
|
||||
|
||||
padding: 2px 8px;
|
||||
|
||||
border-radius: 10px;
|
||||
|
||||
background: #fff7e6;
|
||||
|
||||
border: 1px solid #ffd591;
|
||||
|
||||
font-size: 12px;
|
||||
|
||||
line-height: 1.5;
|
||||
|
||||
color: #d46b08;
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
.im-biz-record-item {
|
||||
|
||||
display: flex;
|
||||
|
||||
flex-direction: column;
|
||||
|
||||
gap: 8px;
|
||||
|
||||
}
|
||||
|
||||
/* 审批办理按钮栏 */
|
||||
.im-biz-record-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding-top: 4px;
|
||||
border-top: 1px dashed #f0f0f0;
|
||||
|
||||
.im-biz-record-done {
|
||||
font-size: 12px;
|
||||
color: #52c41a;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* 不可办理置灰提示 */
|
||||
.im-biz-record-disabled {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 1px 8px;
|
||||
border-radius: 10px;
|
||||
background: #f5f5f5;
|
||||
border: 1px solid #e0e0e0;
|
||||
font-size: 12px;
|
||||
color: #bfbfbf;
|
||||
}
|
||||
}
|
||||
|
||||
.im-biz-record-table-wrap {
|
||||
|
||||
overflow: hidden;
|
||||
|
||||
border: 1px solid #f0f0f0;
|
||||
|
||||
border-radius: 6px;
|
||||
|
||||
background: #fff;
|
||||
|
||||
|
||||
|
||||
&--list {
|
||||
|
||||
overflow-x: auto;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
.im-biz-record-table {
|
||||
|
||||
width: 100%;
|
||||
|
||||
border-collapse: collapse;
|
||||
|
||||
font-size: 13px;
|
||||
|
||||
line-height: 1.5;
|
||||
|
||||
|
||||
|
||||
th,
|
||||
|
||||
td {
|
||||
|
||||
padding: 8px 10px;
|
||||
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
|
||||
vertical-align: top;
|
||||
|
||||
word-break: break-word;
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
tr:last-child {
|
||||
|
||||
th,
|
||||
|
||||
td {
|
||||
|
||||
border-bottom: none;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
&--detail {
|
||||
|
||||
table-layout: fixed;
|
||||
|
||||
|
||||
|
||||
th {
|
||||
|
||||
width: 38%;
|
||||
|
||||
background: #fafafa;
|
||||
|
||||
color: #595959;
|
||||
|
||||
font-weight: 500;
|
||||
|
||||
text-align: left;
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
td {
|
||||
|
||||
color: #262626;
|
||||
|
||||
background: #fff;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
&--list {
|
||||
|
||||
min-width: 100%;
|
||||
|
||||
table-layout: auto;
|
||||
|
||||
|
||||
|
||||
thead th {
|
||||
|
||||
background: #fafafa;
|
||||
|
||||
color: #595959;
|
||||
|
||||
font-weight: 500;
|
||||
|
||||
text-align: left;
|
||||
|
||||
white-space: nowrap;
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
tbody td {
|
||||
|
||||
color: #262626;
|
||||
|
||||
background: #fff;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
.im-biz-record-link-col {
|
||||
|
||||
width: 72px;
|
||||
|
||||
min-width: 72px;
|
||||
|
||||
white-space: nowrap;
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
.im-biz-record-link {
|
||||
|
||||
display: inline-flex;
|
||||
|
||||
align-items: center;
|
||||
|
||||
gap: 4px;
|
||||
|
||||
font-size: 12px;
|
||||
|
||||
color: #1677ff;
|
||||
|
||||
text-decoration: underline;
|
||||
|
||||
cursor: pointer;
|
||||
|
||||
|
||||
|
||||
&:hover {
|
||||
|
||||
color: #0958d9;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
|
||||
@@ -175,6 +175,7 @@
|
||||
:payload="getBizRecordPayload(msg.content)!"
|
||||
:mine="msg.mine"
|
||||
:receiver-has-biz-page-permission="msg.receiverHasBizPagePermission"
|
||||
@handled="handleApprovalHandled"
|
||||
/>
|
||||
<div v-else class="message-content">{{ msg.content }}</div>
|
||||
<div class="message-time">{{ formatTime(msg.createTime) }}</div>
|
||||
@@ -1376,6 +1377,19 @@
|
||||
//update-end---author:xsl ---date:20260528 for:【IM聊天】收到新消息时:靠近底部则自动滚底,否则显示新消息提示-----------
|
||||
}
|
||||
|
||||
//update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】审批办理后即时刷新当前会话,连续节点同一处理人无需关闭重开-----
|
||||
// 审批卡片办理(通过/驳回)成功后:下一节点卡片或结果通知已由后端同步发出并入库,
|
||||
// 此处强制刷新当前会话消息,使新卡片立即出现在当前聊天窗口(不依赖 WS 推送时序)。
|
||||
async function handleApprovalHandled() {
|
||||
if (!activeConversationId.value) {
|
||||
return;
|
||||
}
|
||||
await loadMessages(true, { forceRefresh: true });
|
||||
await nextTick();
|
||||
scrollToBottom();
|
||||
}
|
||||
//update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】审批办理后即时刷新当前会话,连续节点同一处理人无需关闭重开-----
|
||||
|
||||
function handleImChatSocketUi(data: Record<string, any>) {
|
||||
if (data.cmd !== 'chat') {
|
||||
return;
|
||||
|
||||
@@ -16,6 +16,16 @@ export interface ImBizRecordItem {
|
||||
/** v2:表格字段 */
|
||||
fields?: ImBizRecordField[];
|
||||
linkPath: string;
|
||||
// update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】审批卡片办理扩展字段-----
|
||||
/** 审批实例ID(存在则为审批卡片,展示办理按钮) */
|
||||
instanceId?: string;
|
||||
/** 当前节点办理按钮文案(如 审批/审核/批准),存在且 canApprove 时展示 */
|
||||
actionLabel?: string;
|
||||
/** 接收人是否可办理(当前活动处理人 且 审批中) */
|
||||
canApprove?: boolean;
|
||||
/** 该卡片对应的节点ID,用于实时判断是否仍为当前节点(旧节点卡片置灰) */
|
||||
nodeId?: string;
|
||||
// update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】审批卡片办理扩展字段-----
|
||||
}
|
||||
|
||||
export interface ImBizRecordPayload {
|
||||
|
||||
Reference in New Issue
Block a user