pull/155/head
xingyu4j 2025-06-22 14:06:08 +08:00
commit 3a740b5abd
16 changed files with 539 additions and 61 deletions

View File

@ -5,7 +5,10 @@ import type { SimpleFlowNode } from '../../consts';
import { inject, ref } from 'vue'; import { inject, ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { useTaskStatusClass, useWatchNode } from '../../helpers'; import { useTaskStatusClass, useWatchNode } from '../../helpers';
import ProcessInstanceModal from './modules/process-instance-modal.vue';
defineOptions({ name: 'EndEventNode' }); defineOptions({ name: 'EndEventNode' });
const props = defineProps({ const props = defineProps({
@ -20,15 +23,26 @@ const currentNode = useWatchNode(props);
const readonly = inject<Boolean>('readonly'); const readonly = inject<Boolean>('readonly');
const processInstance = inject<Ref<any>>('processInstance', ref({})); const processInstance = inject<Ref<any>>('processInstance', ref({}));
const processInstanceInfos = ref<any[]>([]); // const [Modal, modalApi] = useVbenModal({
connectedComponent: ProcessInstanceModal,
destroyOnClose: true,
});
function nodeClick() { function nodeClick() {
if (readonly && processInstance && processInstance.value) { if (readonly && processInstance && processInstance.value) {
console.warn( const processInstanceInfo = [
'TODO 只读模式,弹窗显示审批信息', {
processInstance.value, startUser: processInstance.value.startUser,
processInstanceInfos.value, createTime: processInstance.value.startTime,
); endTime: processInstance.value.endTime,
status: processInstance.value.status,
durationInMillis: processInstance.value.durationInMillis,
},
];
modalApi
.setData(processInstanceInfo)
.setState({ title: '流程信息' })
.open();
} }
} }
</script> </script>
@ -42,5 +56,6 @@ function nodeClick() {
<span class="node-fixed-name" title="结束">结束</span> <span class="node-fixed-name" title="结束">结束</span>
</div> </div>
</div> </div>
<!-- TODO 审批信息 --> <!-- 流程信息弹窗 -->
<Modal />
</template> </template>

View File

@ -0,0 +1,56 @@
import type { VxeTableGridOptions } from '@vben/plugins/vxe-table';
import { DICT_TYPE } from '#/utils';
/** 流程实例列表字段 */
export function useGridColumns(): VxeTableGridOptions['columns'] {
return [
{
field: 'startUser',
title: '发起人',
slots: {
default: ({ row }: { row: any }) => {
return row.startUser?.nickname;
},
},
minWidth: 100,
},
{
field: 'deptName',
title: '部门',
slots: {
default: ({ row }: { row: any }) => {
return row.startUser?.deptName;
},
},
minWidth: 100,
},
{
field: 'createTime',
title: '开始时间',
formatter: 'formatDateTime',
minWidth: 140,
},
{
field: 'endTime',
title: '结束时间',
formatter: 'formatDateTime',
minWidth: 140,
},
{
field: 'status',
title: '流程状态',
minWidth: 90,
cellRender: {
name: 'CellDict',
props: { type: DICT_TYPE.BPM_PROCESS_INSTANCE_STATUS },
},
},
{
field: 'durationInMillis',
title: '耗时',
minWidth: 100,
formatter: 'formatPast2',
},
];
}

View File

@ -0,0 +1,44 @@
<script lang="ts" setup>
import { useVbenModal } from '@vben/common-ui';
import { useVbenVxeGrid } from '@vben/plugins/vxe-table';
import { useGridColumns } from './process-instance-data';
const [Grid, gridApi] = useVbenVxeGrid({
gridOptions: {
columns: useGridColumns(),
border: true,
height: 'auto',
pagerConfig: {
enabled: false,
},
toolbarConfig: {
enabled: false,
},
},
});
const [Modal, modalApi] = useVbenModal({
footer: false,
async onOpenChange(isOpen: boolean) {
if (!isOpen) {
return;
}
modalApi.lock();
try {
const data = modalApi.getData<any[]>();
//
await gridApi.setGridOptions({ data });
} finally {
modalApi.unlock();
}
},
});
</script>
<template>
<Modal class="w-3/4">
<Grid />
</Modal>
</template>

View File

@ -0,0 +1,61 @@
import type { VxeTableGridOptions } from '@vben/plugins/vxe-table';
import { DICT_TYPE } from '#/utils';
/** 审批记录列表字段 */
export function useGridColumns(): VxeTableGridOptions['columns'] {
return [
{
field: 'assigneeUser',
title: '审批人',
slots: {
default: ({ row }: { row: any }) => {
return row.assigneeUser?.nickname || row.ownerUser?.nickname;
},
},
minWidth: 100,
},
{
field: 'deptName',
title: '部门',
slots: {
default: ({ row }: { row: any }) => {
return row.assigneeUser?.deptName || row.ownerUser?.deptName;
},
},
minWidth: 100,
},
{
field: 'createTime',
title: '开始时间',
formatter: 'formatDateTime',
minWidth: 140,
},
{
field: 'endTime',
title: '结束时间',
formatter: 'formatDateTime',
minWidth: 140,
},
{
field: 'status',
title: '审批状态',
minWidth: 90,
cellRender: {
name: 'CellDict',
props: { type: DICT_TYPE.BPM_TASK_STATUS },
},
},
{
field: 'reason',
title: '审批建议',
minWidth: 160,
},
{
field: 'durationInMillis',
title: '耗时',
minWidth: 100,
formatter: 'formatPast2',
},
];
}

View File

@ -0,0 +1,47 @@
<script lang="ts" setup>
import { useVbenModal } from '@vben/common-ui';
import { useVbenVxeGrid } from '@vben/plugins/vxe-table';
import { useGridColumns } from './task-list-data';
const [Grid, gridApi] = useVbenVxeGrid({
gridOptions: {
columns: useGridColumns(),
border: true,
height: 'auto',
rowConfig: {
keyField: 'id',
},
pagerConfig: {
enabled: false,
},
toolbarConfig: {
enabled: false,
},
},
});
const [Modal, modalApi] = useVbenModal({
footer: false,
async onOpenChange(isOpen: boolean) {
if (!isOpen) {
return;
}
modalApi.lock();
try {
const data = modalApi.getData<any[]>();
//
await gridApi.setGridOptions({ data });
} finally {
modalApi.unlock();
}
},
});
</script>
<template>
<Modal class="w-3/4">
<Grid />
</Modal>
</template>

View File

@ -6,6 +6,7 @@ import type { SimpleFlowNode } from '../../consts';
import { inject, ref } from 'vue'; import { inject, ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { IconifyIcon } from '@vben/icons'; import { IconifyIcon } from '@vben/icons';
import { Input } from 'ant-design-vue'; import { Input } from 'ant-design-vue';
@ -15,6 +16,7 @@ import { BpmNodeTypeEnum } from '#/utils';
import { NODE_DEFAULT_TEXT } from '../../consts'; import { NODE_DEFAULT_TEXT } from '../../consts';
import { useNodeName2, useTaskStatusClass, useWatchNode } from '../../helpers'; import { useNodeName2, useTaskStatusClass, useWatchNode } from '../../helpers';
import StartUserNodeConfig from '../nodes-config/start-user-node-config.vue'; import StartUserNodeConfig from '../nodes-config/start-user-node-config.vue';
import TaskListModal from './modules/task-list-modal.vue';
import NodeHandler from './node-handler.vue'; import NodeHandler from './node-handler.vue';
defineOptions({ name: 'StartUserNode' }); defineOptions({ name: 'StartUserNode' });
@ -27,7 +29,6 @@ const props = defineProps({
}); });
// //
// const emits = defineEmits<{
defineEmits<{ defineEmits<{
'update:modelValue': [node: SimpleFlowNode | undefined]; 'update:modelValue': [node: SimpleFlowNode | undefined];
}>(); }>();
@ -44,24 +45,25 @@ const { showInput, changeNodeName, clickTitle, inputRef } = useNodeName2(
const nodeSetting = ref(); const nodeSetting = ref();
// const [Modal, modalApi] = useVbenModal({
const selectTasks = ref<any[] | undefined>([]); // connectedComponent: TaskListModal,
destroyOnClose: true,
});
function nodeClick() { function nodeClick() {
if (readonly) { if (readonly) {
// //
if (tasks && tasks.value) { if (tasks && tasks.value) {
console.warn( //
'TODO 只读模式,弹窗显示任务信息', const nodeTasks = tasks.value.filter(
tasks.value, (task) => task.taskDefinitionKey === currentNode.value.id,
selectTasks.value,
); );
//
modalApi
.setData(nodeTasks)
.setState({ title: currentNode.value.name })
.open();
} }
} else { } else {
console.warn(
'TODO 编辑模式,打开节点配置、把当前节点传递给配置组件',
nodeSetting.value,
);
nodeSetting.value.showStartUserNodeConfig(currentNode.value); nodeSetting.value.showStartUserNodeConfig(currentNode.value);
} }
} }
@ -122,5 +124,6 @@ function nodeClick() {
ref="nodeSetting" ref="nodeSetting"
:flow-node="currentNode" :flow-node="currentNode"
/> />
<!-- 审批记录 TODO --> <!-- 审批记录弹窗 -->
<Modal />
</template> </template>

View File

@ -5,6 +5,7 @@ import type { SimpleFlowNode } from '../../consts';
import { inject, ref } from 'vue'; import { inject, ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { IconifyIcon } from '@vben/icons'; import { IconifyIcon } from '@vben/icons';
import { Input } from 'ant-design-vue'; import { Input } from 'ant-design-vue';
@ -14,6 +15,26 @@ import { BpmNodeTypeEnum } from '#/utils';
import { NODE_DEFAULT_TEXT } from '../../consts'; import { NODE_DEFAULT_TEXT } from '../../consts';
import { useNodeName2, useTaskStatusClass, useWatchNode } from '../../helpers'; import { useNodeName2, useTaskStatusClass, useWatchNode } from '../../helpers';
import UserTaskNodeConfig from '../nodes-config/user-task-node-config.vue'; import UserTaskNodeConfig from '../nodes-config/user-task-node-config.vue';
import TaskListModal from './modules/task-list-modal.vue';
// // 使useVbenVxeGrid
// const [Grid, gridApi] = useVbenVxeGrid({
// gridOptions: {
// columns: columns.value,
// keepSource: true,
// border: true,
// height: 'auto',
// data: selectTasks.value,
// rowConfig: {
// keyField: 'id',
// },
// pagerConfig: {
// enabled: false,
// },
// toolbarConfig: {
// enabled: false,
// },
// } as VxeTableGridOptions<any>,
// });
import NodeHandler from './node-handler.vue'; import NodeHandler from './node-handler.vue';
defineOptions({ name: 'UserTaskNode' }); defineOptions({ name: 'UserTaskNode' });
@ -42,11 +63,23 @@ const { showInput, changeNodeName, clickTitle, inputRef } = useNodeName2(
); );
const nodeSetting = ref(); const nodeSetting = ref();
const [Modal, modalApi] = useVbenModal({
connectedComponent: TaskListModal,
destroyOnClose: true,
});
function nodeClick() { function nodeClick() {
if (readonly) { if (readonly) {
if (tasks && tasks.value) { if (tasks && tasks.value) {
// TODO //
console.warn('只读模式,弹窗显示任务信息待实现'); const nodeTasks = tasks.value.filter(
(task) => task.taskDefinitionKey === currentNode.value.id,
);
//
modalApi
.setData(nodeTasks)
.setState({ title: currentNode.value.name })
.open();
} }
} else { } else {
// //
@ -64,8 +97,6 @@ function findReturnTaskNodes(
// //
emits('findParentNode', matchNodeList, BpmNodeTypeEnum.USER_TASK_NODE); emits('findParentNode', matchNodeList, BpmNodeTypeEnum.USER_TASK_NODE);
} }
// const selectTasks = ref<any[] | undefined>([]); //
</script> </script>
<template> <template>
<div class="node-wrapper"> <div class="node-wrapper">
@ -138,5 +169,6 @@ function findReturnTaskNodes(
:flow-node="currentNode" :flow-node="currentNode"
@find-return-task-nodes="findReturnTaskNodes" @find-return-task-nodes="findReturnTaskNodes"
/> />
<!-- TODO 审批记录 --> <!-- 审批记录弹窗 -->
<Modal />
</template> </template>

View File

@ -249,7 +249,7 @@ onMounted(() => {
/> />
</div> </div>
</div> </div>
<!-- TODO 这个好像暂时没有用到保存失败弹窗 -->
<Modal <Modal
v-model:open="errorDialogVisible" v-model:open="errorDialogVisible"
title="保存失败" title="保存失败"

View File

@ -0,0 +1,45 @@
<script setup lang="ts">
import type { SimpleFlowNode } from '../consts';
import { provide, ref, watch } from 'vue';
import { useWatchNode } from '../helpers';
import SimpleProcessModel from './simple-process-model.vue';
defineOptions({ name: 'SimpleProcessViewer' });
const props = withDefaults(
defineProps<{
flowNode: SimpleFlowNode;
//
processInstance?: any;
//
tasks?: any[];
}>(),
{
processInstance: undefined,
tasks: () => [] as any[],
},
);
const approveTasks = ref<any[]>(props.tasks);
const currentProcessInstance = ref(props.processInstance);
const simpleModel = useWatchNode(props);
watch(
() => props.tasks,
(newValue) => {
approveTasks.value = newValue;
},
);
watch(
() => props.processInstance,
(newValue) => {
currentProcessInstance.value = newValue;
},
);
// 使
provide('tasks', approveTasks);
provide('processInstance', currentProcessInstance);
</script>
<template>
<SimpleProcessModel :flow-node="simpleModel" :readonly="true" />
</template>

View File

@ -4,4 +4,8 @@ export { default as HttpRequestSetting } from './components/nodes-config/modules
export { default as SimpleProcessDesigner } from './components/simple-process-designer.vue'; export { default as SimpleProcessDesigner } from './components/simple-process-designer.vue';
export { default as SimpleProcessViewer } from './components/simple-process-viewer.vue';
export type { SimpleFlowNode } from './consts';
export { parseFormFields } from './helpers'; export { parseFormFields } from './helpers';

View File

@ -46,6 +46,7 @@ const processedActions = ref<any[]>([]);
const processedDropdownActions = ref<any[]>([]); const processedDropdownActions = ref<any[]>([]);
/** 用于比较的字符串化版本 */ /** 用于比较的字符串化版本 */
// TODO @xingyu
const actionsStringified = ref(''); const actionsStringified = ref('');
const dropdownActionsStringified = ref(''); const dropdownActionsStringified = ref('');

View File

@ -346,7 +346,12 @@ onMounted(async () => {
</Row> </Row>
</TabPane> </TabPane>
<TabPane tab="流程图" key="diagram" class="tab-pane-content"> <TabPane
tab="流程图"
key="diagram"
class="tab-pane-content"
:force-render="true"
>
<div class="h-full"> <div class="h-full">
<ProcessInstanceSimpleViewer <ProcessInstanceSimpleViewer
v-show=" v-show="

View File

@ -1,9 +1,180 @@
<script setup lang="ts"> <script lang="ts" setup>
defineOptions({ name: 'ProcessInstanceSimpleViewer' }); import type { SimpleFlowNode } from '#/components/simple-process-design';
</script>
import { ref, watch } from 'vue';
import { SimpleProcessViewer } from '#/components/simple-process-design';
import { BpmNodeTypeEnum, BpmTaskStatusEnum } from '#/utils';
defineOptions({ name: 'BpmProcessInstanceSimpleViewer' });
const props = withDefaults(
defineProps<{
loading?: boolean; //
modelView?: any;
simpleJson?: string; // Simple (json )
}>(),
{
loading: false,
modelView: () => ({}),
simpleJson: '',
},
);
const simpleModel = ref<any>({});
//
const tasks = ref([]);
//
const processInstance = ref();
/** 监控模型视图 包括任务列表、进行中的活动节点编号等 */
watch(
() => props.modelView,
async (newModelView) => {
if (newModelView) {
tasks.value = newModelView.tasks;
processInstance.value = newModelView.processInstance;
// UserTask
const rejectedTaskActivityIds: string[] =
newModelView.rejectedTaskActivityIds;
// UserTask
const unfinishedTaskActivityIds: string[] =
newModelView.unfinishedTaskActivityIds;
// UserTaskGateway
const finishedActivityIds: string[] =
newModelView.finishedTaskActivityIds;
// 线 SequenceFlow
const finishedSequenceFlowActivityIds: string[] =
newModelView.finishedSequenceFlowActivityIds;
setSimpleModelNodeTaskStatus(
newModelView.simpleModel,
newModelView.processInstance?.status,
rejectedTaskActivityIds,
unfinishedTaskActivityIds,
finishedActivityIds,
finishedSequenceFlowActivityIds,
);
simpleModel.value = newModelView.simpleModel || {};
}
},
);
/** 监控模型结构数据 */
watch(
() => props.simpleJson,
async (value) => {
if (value) {
simpleModel.value = JSON.parse(value);
}
},
);
const setSimpleModelNodeTaskStatus = (
simpleModel: SimpleFlowNode | undefined,
processStatus: number,
rejectedTaskActivityIds: string[],
unfinishedTaskActivityIds: string[],
finishedActivityIds: string[],
finishedSequenceFlowActivityIds: string[],
) => {
if (!simpleModel) {
return;
}
//
if (simpleModel.type === BpmNodeTypeEnum.END_EVENT_NODE) {
simpleModel.activityStatus = finishedActivityIds.includes(simpleModel.id)
? processStatus
: BpmTaskStatusEnum.NOT_START;
return;
}
//
if (
simpleModel.type === BpmNodeTypeEnum.START_USER_NODE ||
simpleModel.type === BpmNodeTypeEnum.USER_TASK_NODE ||
simpleModel.type === BpmNodeTypeEnum.TRANSACTOR_NODE ||
simpleModel.type === BpmNodeTypeEnum.CHILD_PROCESS_NODE
) {
simpleModel.activityStatus = BpmTaskStatusEnum.NOT_START;
if (rejectedTaskActivityIds.includes(simpleModel.id)) {
simpleModel.activityStatus = BpmTaskStatusEnum.REJECT;
} else if (unfinishedTaskActivityIds.includes(simpleModel.id)) {
simpleModel.activityStatus = BpmTaskStatusEnum.RUNNING;
} else if (finishedActivityIds.includes(simpleModel.id)) {
simpleModel.activityStatus = BpmTaskStatusEnum.APPROVE;
}
// TODO cancel
}
//
if (simpleModel.type === BpmNodeTypeEnum.COPY_TASK_NODE) {
// ,
simpleModel.activityStatus = finishedActivityIds.includes(simpleModel.id)
? BpmTaskStatusEnum.APPROVE
: BpmTaskStatusEnum.NOT_START;
}
//
if (simpleModel.type === BpmNodeTypeEnum.DELAY_TIMER_NODE) {
// ,
simpleModel.activityStatus = finishedActivityIds.includes(simpleModel.id)
? BpmTaskStatusEnum.APPROVE
: BpmTaskStatusEnum.NOT_START;
}
//
if (simpleModel.type === BpmNodeTypeEnum.TRIGGER_NODE) {
// ,
simpleModel.activityStatus = finishedActivityIds.includes(simpleModel.id)
? BpmTaskStatusEnum.APPROVE
: BpmTaskStatusEnum.NOT_START;
}
// SequenceFlow
if (simpleModel.type === BpmNodeTypeEnum.CONDITION_NODE) {
// ,
simpleModel.activityStatus = finishedSequenceFlowActivityIds.includes(
simpleModel.id,
)
? BpmTaskStatusEnum.APPROVE
: BpmTaskStatusEnum.NOT_START;
}
//
if (
simpleModel.type === BpmNodeTypeEnum.CONDITION_BRANCH_NODE ||
simpleModel.type === BpmNodeTypeEnum.PARALLEL_BRANCH_NODE ||
simpleModel.type === BpmNodeTypeEnum.INCLUSIVE_BRANCH_NODE ||
simpleModel.type === BpmNodeTypeEnum.ROUTER_BRANCH_NODE
) {
//
simpleModel.activityStatus = finishedActivityIds.includes(simpleModel.id)
? BpmTaskStatusEnum.APPROVE
: BpmTaskStatusEnum.NOT_START;
simpleModel.conditionNodes?.forEach((node) => {
setSimpleModelNodeTaskStatus(
node,
processStatus,
rejectedTaskActivityIds,
unfinishedTaskActivityIds,
finishedActivityIds,
finishedSequenceFlowActivityIds,
);
});
}
setSimpleModelNodeTaskStatus(
simpleModel.childNode,
processStatus,
rejectedTaskActivityIds,
unfinishedTaskActivityIds,
finishedActivityIds,
finishedSequenceFlowActivityIds,
);
};
</script>
<template> <template>
<div> <div v-loading="loading">
<h1>Simple BPM Viewer</h1> <SimpleProcessViewer
:flow-node="simpleModel"
:tasks="tasks"
:process-instance="processInstance"
/>
</div> </div>
</template> </template>
<style lang="scss" scoped></style>

View File

@ -2,10 +2,10 @@
import type { VxeTableGridOptions } from '#/adapter/vxe-table'; import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { BpmProcessInstanceApi } from '#/api/bpm/processInstance'; import type { BpmProcessInstanceApi } from '#/api/bpm/processInstance';
import { h, nextTick, onMounted, ref } from 'vue'; import { nextTick, onMounted, ref } from 'vue';
import { useRoute, useRouter } from 'vue-router'; import { useRoute, useRouter } from 'vue-router';
import { confirm, Page } from '@vben/common-ui'; import { Page, prompt } from '@vben/common-ui';
import { Input, message } from 'ant-design-vue'; import { Input, message } from 'ant-design-vue';
@ -29,7 +29,6 @@ const processDefinitionId = query.processDefinitionId as string;
const formFields = ref<any[]>([]); const formFields = ref<any[]>([]);
const userList = ref<any[]>([]); // const userList = ref<any[]>([]); //
const gridReady = ref(false); // const gridReady = ref(false); //
const cancelReason = ref(''); //
// //
let Grid: any = null; let Grid: any = null;
@ -81,26 +80,19 @@ const handleDetail = (row: BpmProcessInstanceApi.ProcessInstance) => {
/** 取消按钮操作 */ /** 取消按钮操作 */
const handleCancel = async (row: BpmProcessInstanceApi.ProcessInstance) => { const handleCancel = async (row: BpmProcessInstanceApi.ProcessInstance) => {
cancelReason.value = ''; // prompt({
confirm({ content: '请输入取消原因:',
title: '取消流程', title: '取消流程',
content: h('div', [ icon: 'question',
h('p', '请输入取消原因:'), component: Input,
h(Input, { modelPropName: 'value',
value: cancelReason.value, async beforeClose(scope) {
'onUpdate:value': (val: string) => { if (!scope.isConfirm) return;
cancelReason.value = val; if (!scope.value) {
},
placeholder: '请输入取消原因',
}),
]),
beforeClose: async ({ isConfirm }) => {
if (!isConfirm) return;
if (!cancelReason.value.trim()) {
message.warning('请输入取消原因'); message.warning('请输入取消原因');
return false; return false;
} }
await cancelProcessInstanceByAdmin(row.id, cancelReason.value); await cancelProcessInstanceByAdmin(row.id, scope.value);
return true; return true;
}, },
}).then(() => { }).then(() => {

View File

@ -29,6 +29,7 @@ const [Modal, modalApi] = useVbenModal({
<Modal class="w-2/5" :title="$t('ui.widgets.qa')"> <Modal class="w-2/5" :title="$t('ui.widgets.qa')">
<div class="mt-2 flex flex-col"> <div class="mt-2 flex flex-col">
<div class="mt-2 flex flex-row"> <div class="mt-2 flex flex-row">
<!-- TODO @xingyu要不要垂直1. 项目地址2. 问题反馈3. 开发文档 -->
<VbenButtonGroup class="basis-1/3" :gap="2" border size="large"> <VbenButtonGroup class="basis-1/3" :gap="2" border size="large">
<p class="p-2">项目地址:</p> <p class="p-2">项目地址:</p>
<VbenButton <VbenButton

View File

@ -46,6 +46,7 @@ async function handleChange(id: number | undefined) {
} }
</script> </script>
<template> <template>
<!-- TODO @xingyu1未选择的时候空着一块有点怪是不是有个 placeholder 会好看点哈之前有 page.tenant.placeholder2是不是要支持个 clear 选择 -->
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger as-child> <DropdownMenuTrigger as-child>
<Button <Button