Merge remote-tracking branch 'origin/20260519-3.9.2版本-葛昊天分支'

This commit is contained in:
2026-05-29 15:51:29 +08:00
48 changed files with 5603 additions and 261 deletions

View File

@@ -0,0 +1,128 @@
<!--
审批流设计 列表页
@author GHT
@date 2026-05-29 forQH-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>

View File

@@ -0,0 +1,51 @@
<!--
审批流 基本信息 新增/编辑弹窗先选单据
@author GHT
@date 2026-05-29 forQH-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>

View 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审批流设计】当前页设计上下文(解析阶段字段+取/建草稿流程)-----

View 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,
},
},
];

View 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 });

View File

@@ -0,0 +1,189 @@
<!--
钉钉式审批流 可视化设计器全屏
@author GHT
@date 2026-05-29 forQH-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 forQH-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 forQH-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>

View File

@@ -0,0 +1,99 @@
<!--
钉钉式审批流 递归节点组件
@author GHT
@date 2026-05-29 forQH-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>

View File

@@ -0,0 +1,359 @@
<!--
审批流节点配置抽屉
@author GHT
@date 2026-05-29 forQH-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 forQH-MES审批流设计取单据字段审批人----- -->
<a-radio value="field">取单据字段</a-radio>
<!-- update-end---author:GHT ---date:2026-05-29 forQH-MES审批流设计取单据字段审批人----- -->
</a-radio-group>
</a-form-item>
<!-- update-begin---author:GHT ---date:2026-05-29 forQH-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 forQH-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 forQH-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 forQH-MES审批流设计节点回调接口可视化配置(录制业务按钮接口)----- -->
</template>
<!-- 抄送人 -->
<template v-else-if="node?.type === 'cc'">
<!-- update-begin---author:GHT ---date:2026-05-29 forQH-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 forQH-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 forQH-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 forQH-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>

View 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;
}
}

View 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);
}
}
});
}

View 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 });

View File

@@ -0,0 +1,154 @@
<!--
IM 审批卡片查看详情弹窗展示单据全部字段 + 审批进度/历史
@author GHT
@date 2026-05-29 forQH-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>

View File

@@ -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>

View File

@@ -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;

View File

@@ -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 {