This commit is contained in:
gjd
2025-06-16 09:58:35 +08:00
648 changed files with 27087 additions and 5714 deletions

View File

@@ -23,6 +23,11 @@ interface DictTagProps {
const props = defineProps<DictTagProps>();
function isHexColor(color: string) {
const reg = /^#(?:[0-9a-f]{3}|[0-9a-f]{6})$/i;
return reg.test(color);
}
/** 获取字典标签 */
const dictTag = computed(() => {
// 校验参数有效性
@@ -66,7 +71,16 @@ const dictTag = computed(() => {
</script>
<template>
<Tag v-if="dictTag" :color="dictTag.colorType">
<Tag
v-if="dictTag"
:color="
dictTag.colorType
? dictTag.colorType
: dictTag.cssClass && isHexColor(dictTag.cssClass)
? dictTag.cssClass
: ''
"
>
{{ dictTag.label }}
</Tag>
</template>

View File

@@ -1,34 +0,0 @@
<script lang="ts" setup>
import { isDocAlertEnable } from '@vben/hooks';
import { openWindow } from '@vben/utils';
import { Alert, Typography } from 'ant-design-vue';
export interface DocAlertProps {
/**
* 文档标题
*/
title: string;
/**
* 文档 URL 地址
*/
url: string;
}
const props = defineProps<DocAlertProps>();
/** 跳转 URL 链接 */
const goToUrl = () => {
openWindow(props.url);
};
</script>
<template>
<Alert v-if="isDocAlertEnable()" type="info" show-icon class="mb-2 rounded">
<template #message>
<Typography.Link @click="goToUrl">
{{ title }}文档地址{{ url }}
</Typography.Link>
</template>
</Alert>
</template>

View File

@@ -1 +0,0 @@
export { default as DocAlert } from './doc-alert.vue';

View File

@@ -1,3 +1,9 @@
import { defineAsyncComponent } from 'vue';
export const AsyncOperateLog = defineAsyncComponent(
() => import('./operate-log.vue'),
);
export { default as OperateLog } from './operate-log.vue';
export type { OperateLogProps } from './typing';

View File

@@ -1,7 +1,9 @@
<script setup lang="ts">
import type { OperateLogProps } from './typing';
import { Timeline } from 'ant-design-vue';
import { formatDateTime } from '@vben/utils';
import { Tag, Timeline } from 'ant-design-vue';
import { DICT_TYPE, getDictLabel, getDictObj } from '#/utils';
@@ -38,8 +40,21 @@ function getUserTypeColor(userType: number) {
:key="log.id"
:color="getUserTypeColor(log.userType)"
>
<p>{{ log.createTime }}</p>
<p>{{ getDictLabel(DICT_TYPE.USER_TYPE, log.userType)[0] }}</p>
<template #dot>
<p
:style="{ backgroundColor: getUserTypeColor(log.userType) }"
class="absolute left-[-5px] flex h-5 w-5 items-center justify-center rounded-full text-xs text-white"
>
{{ getDictLabel(DICT_TYPE.USER_TYPE, log.userType)[0] }}
</p>
</template>
<p>{{ formatDateTime(log.createTime) }}</p>
<p>
<Tag :color="getUserTypeColor(log.userType)">
{{ log.userName }}
</Tag>
{{ log.action }}
</p>
</Timeline.Item>
</Timeline>
</div>

View File

@@ -9,7 +9,7 @@ import { ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { handleTree } from '@vben/utils';
import { Button, Card, Col, Row, Tree } from 'ant-design-vue';
import { Card, Col, Row, Tree } from 'ant-design-vue';
import { getSimpleDeptList } from '#/api/system/dept';
@@ -41,24 +41,6 @@ const emit = defineEmits<{
confirm: [deptList: SystemDeptApi.Dept[]];
}>();
// 对话框配置
const [Modal, modalApi] = useVbenModal({
title: props.title,
async onOpenChange(isOpen: boolean) {
if (!isOpen) {
resetData();
return;
}
modalApi.setState({ loading: true });
try {
deptData.value = await getSimpleDeptList();
deptTree.value = handleTree(deptData.value) as DataNode[];
} finally {
modalApi.setState({ loading: false });
}
},
destroyOnClose: true,
});
type checkedKeys = number[] | { checked: number[]; halfChecked: number[] };
// 部门树形结构
const deptTree = ref<DataNode[]>([]);
@@ -67,25 +49,56 @@ const selectedDeptIds = ref<checkedKeys>([]);
// 部门数据
const deptData = ref<SystemDeptApi.Dept[]>([]);
/** 打开对话框 */
const open = async (selectedList?: SystemDeptApi.Dept[]) => {
modalApi.open();
// // 设置已选择的部门
if (selectedList?.length) {
const selectedIds = selectedList
.map((dept) => dept.id)
.filter((id): id is number => id !== undefined);
selectedDeptIds.value = props.checkStrictly
? {
checked: selectedIds,
halfChecked: [],
}
: selectedIds;
}
};
// 对话框配置
const [Modal, modalApi] = useVbenModal({
async onConfirm() {
// 获取选中的部门ID
const selectedIds: number[] = Array.isArray(selectedDeptIds.value)
? selectedDeptIds.value
: selectedDeptIds.value.checked || [];
const deptArray = deptData.value.filter((dept) =>
selectedIds.includes(dept.id!),
);
emit('confirm', deptArray);
// 关闭并提示
await modalApi.close();
},
async onOpenChange(isOpen: boolean) {
if (!isOpen) {
deptTree.value = [];
selectedDeptIds.value = [];
return;
}
// 加载数据
const data = modalApi.getData();
if (!data) {
return;
}
modalApi.lock();
try {
deptData.value = await getSimpleDeptList();
deptTree.value = handleTree(deptData.value) as DataNode[];
// // 设置已选择的部门
if (data.selectedList?.length) {
const selectedIds = data.selectedList
.map((dept: SystemDeptApi.Dept) => dept.id)
.filter((id: number) => id !== undefined);
selectedDeptIds.value = props.checkStrictly
? {
checked: selectedIds,
halfChecked: [],
}
: selectedIds;
}
} finally {
modalApi.unlock();
}
},
destroyOnClose: true,
});
/** 处理选中状态变化 */
const handleCheck = () => {
function handleCheck() {
if (!props.multiple) {
// 单选模式下,只保留最后选择的节点
if (Array.isArray(selectedDeptIds.value)) {
@@ -106,37 +119,10 @@ const handleCheck = () => {
}
}
}
};
/** 提交选择 */
const handleConfirm = async () => {
// 获取选中的部门ID
const selectedIds: number[] = Array.isArray(selectedDeptIds.value)
? selectedDeptIds.value
: selectedDeptIds.value.checked || [];
const deptArray = deptData.value.filter((dept) =>
selectedIds.includes(dept.id!),
);
// 关闭并提示
await modalApi.close();
emit('confirm', deptArray);
};
const handleCancel = () => {
modalApi.close();
};
/** 重置数据 */
const resetData = () => {
deptTree.value = [];
selectedDeptIds.value = [];
};
/** 提供 open 方法,用于打开对话框 */
defineExpose({ open });
}
</script>
<template>
<Modal>
<Modal :title="title" key="dept-select-modal" class="w-[40%]">
<Row class="h-full">
<Col :span="24">
<Card class="h-full">
@@ -153,9 +139,5 @@ defineExpose({ open });
</Card>
</Col>
</Row>
<template #footer>
<Button @click="handleCancel">{{ cancelText }}</Button>
<Button type="primary" @click="handleConfirm">{{ confirmText }}</Button>
</template>
</Modal>
</template>

View File

@@ -1,5 +1,6 @@
<script lang="ts" setup>
// TODO @芋艿:是否有更好的组织形式?!
// TODO @xingyu你感觉这个放到每个 system、infra 模块下,然后新建一个 components表示每个模块有一些共享的组件然后全局只放通用的无业务含义的可以哇
import type { Key } from 'ant-design-vue/es/table/interface';
import type { SystemDeptApi } from '#/api/system/dept';
@@ -17,7 +18,6 @@ import {
message,
Pagination,
Row,
Spin,
Transfer,
Tree,
} from 'ant-design-vue';
@@ -35,7 +35,7 @@ interface DeptTreeNode {
defineOptions({ name: 'UserSelectModal' });
const props = withDefaults(
withDefaults(
defineProps<{
cancelText?: string;
confirmText?: string;
@@ -66,16 +66,66 @@ const expandedKeys = ref<Key[]>([]);
const selectedDeptId = ref<number>();
const deptSearchKeys = ref('');
// 加载状态
const loading = ref(false);
// 用户数据管理
const userList = ref<SystemUserApi.User[]>([]); // 存储所有已知用户
const selectedUserIds = ref<string[]>([]);
// 弹窗配置
const [Modal, modalApi] = useVbenModal({
onCancel: handleCancel,
onClosed: handleClosed,
async onOpenChange(isOpen: boolean) {
if (!isOpen) {
resetData();
return;
}
// 加载数据
const data = modalApi.getData();
if (!data) {
return;
}
modalApi.lock();
try {
// 加载部门数据
const deptData = await getSimpleDeptList();
deptList.value = deptData;
const treeData = handleTree(deptData);
deptTree.value = treeData.map((node) => processDeptNode(node));
expandedKeys.value = deptTree.value.map((node) => node.key);
// 加载初始用户数据
await loadUserData(1, leftListState.value.pagination.pageSize);
// 设置已选用户
if (data.userIds?.length) {
selectedUserIds.value = data.userIds.map(String);
// 加载已选用户的完整信息 TODO 目前接口暂不支持 多个用户ID 查询, 需要后端支持
const { list } = await getUserPage({
pageNo: 1,
pageSize: 100, // 临时使用固定值确保能加载所有已选用户
userIds: data.userIds,
});
// 使用 Map 来去重,以用户 ID 为 key
const userMap = new Map(userList.value.map((user) => [user.id, user]));
list.forEach((user) => {
if (!userMap.has(user.id)) {
userMap.set(user.id, user);
}
});
userList.value = [...userMap.values()];
updateRightListData();
}
modalApi.open();
} finally {
modalApi.unlock();
}
},
destroyOnClose: true,
});
// 左侧列表状态
const leftListState = ref({
loading: false,
searchValue: '',
dataSource: [] as SystemUserApi.User[],
pagination: {
@@ -145,8 +195,7 @@ const filteredDeptTree = computed(() => {
});
// 加载用户数据
const loadUserData = async (pageNo: number, pageSize: number) => {
leftListState.value.loading = true;
async function loadUserData(pageNo: number, pageSize: number) {
try {
const { list, total } = await getUserPage({
pageNo,
@@ -168,12 +217,12 @@ const loadUserData = async (pageNo: number, pageSize: number) => {
userList.value.push(...newUsers);
}
} finally {
leftListState.value.loading = false;
//
}
};
}
// 更新右侧列表数据
const updateRightListData = () => {
function updateRightListData() {
// 使用 Set 来去重选中的用户ID
const uniqueSelectedIds = new Set(selectedUserIds.value);
@@ -202,22 +251,22 @@ const updateRightListData = () => {
const endIndex = startIndex + pageSize;
rightListState.value.dataSource = filteredUsers.slice(startIndex, endIndex);
};
}
// 处理左侧分页变化
const handleLeftPaginationChange = async (page: number, pageSize: number) => {
async function handleLeftPaginationChange(page: number, pageSize: number) {
await loadUserData(page, pageSize);
};
}
// 处理右侧分页变化
const handleRightPaginationChange = (page: number, pageSize: number) => {
function handleRightPaginationChange(page: number, pageSize: number) {
rightListState.value.pagination.current = page;
rightListState.value.pagination.pageSize = pageSize;
updateRightListData();
};
}
// 处理用户搜索
const handleUserSearch = async (direction: string, value: string) => {
async function handleUserSearch(direction: string, value: string) {
if (direction === 'left') {
leftListState.value.searchValue = value;
leftListState.value.pagination.current = 1;
@@ -227,18 +276,18 @@ const handleUserSearch = async (direction: string, value: string) => {
rightListState.value.pagination.current = 1;
updateRightListData();
}
};
}
// 处理用户选择变化
const handleUserChange = (targetKeys: string[]) => {
function handleUserChange(targetKeys: string[]) {
// 使用 Set 来去重选中的用户ID
selectedUserIds.value = [...new Set(targetKeys)];
emit('update:value', selectedUserIds.value.map(Number));
updateRightListData();
};
}
// 重置数据
const resetData = () => {
function resetData() {
userList.value = [];
selectedUserIds.value = [];
@@ -249,7 +298,6 @@ const resetData = () => {
selectedUserIds.value = [];
leftListState.value = {
loading: false,
searchValue: '',
dataSource: [],
pagination: {
@@ -268,61 +316,20 @@ const resetData = () => {
total: 0,
},
};
};
// 打开弹窗
const open = async (userIds: string[]) => {
resetData();
loading.value = true;
try {
// 加载部门数据
const deptData = await getSimpleDeptList();
deptList.value = deptData;
const treeData = handleTree(deptData);
deptTree.value = treeData.map((node) => processDeptNode(node));
expandedKeys.value = deptTree.value.map((node) => node.key);
// 加载初始用户数据
await loadUserData(1, leftListState.value.pagination.pageSize);
// 设置已选用户
if (userIds?.length) {
selectedUserIds.value = userIds.map(String);
// 加载已选用户的完整信息 TODO 目前接口暂不支持 多个用户ID 查询, 需要后端支持
const { list } = await getUserPage({
pageNo: 1,
pageSize: 100, // 临时使用固定值确保能加载所有已选用户
userIds,
});
// 使用 Map 来去重,以用户 ID 为 key
const userMap = new Map(userList.value.map((user) => [user.id, user]));
list.forEach((user) => {
if (!userMap.has(user.id)) {
userMap.set(user.id, user);
}
});
userList.value = [...userMap.values()];
updateRightListData();
}
modalApi.open();
} finally {
loading.value = false;
}
};
}
// TODO 后端接口目前仅支持 username 检索, 筛选条件需要跟后端请求参数保持一致。
const filterOption = (inputValue: string, option: any) => {
function filterOption(inputValue: string, option: any) {
return option.username.toLowerCase().includes(inputValue.toLowerCase());
};
}
// 处理部门树展开/折叠
const handleExpand = (keys: Key[]) => {
function handleExpand(keys: Key[]) {
expandedKeys.value = keys;
};
}
// 处理部门搜索
const handleDeptSearch = (value: string) => {
function handleDeptSearch(value: string) {
deptSearchKeys.value = value;
// 如果有搜索结果,自动展开所有节点
@@ -342,10 +349,10 @@ const handleDeptSearch = (value: string) => {
// 清空搜索时,只展开第一级节点
expandedKeys.value = deptTree.value.map((node) => node.key);
}
};
}
// 处理部门选择
const handleDeptSelect = async (selectedKeys: Key[], _info: any) => {
async function handleDeptSelect(selectedKeys: Key[], _info: any) {
// 更新选中的部门ID
const newDeptId =
selectedKeys.length > 0 ? Number(selectedKeys[0]) : undefined;
@@ -356,10 +363,10 @@ const handleDeptSelect = async (selectedKeys: Key[], _info: any) => {
const { pageSize } = leftListState.value.pagination;
leftListState.value.pagination.current = 1;
await loadUserData(1, pageSize);
};
}
// 确认选择
const handleConfirm = () => {
function handleConfirm() {
if (selectedUserIds.value.length === 0) {
message.warning('请选择用户');
return;
@@ -371,115 +378,101 @@ const handleConfirm = () => {
),
);
modalApi.close();
};
}
// 取消选择
const handleCancel = () => {
function handleCancel() {
emit('cancel');
modalApi.close();
// 确保在动画结束后再重置数据
setTimeout(() => {
resetData();
}, 300);
};
}
// 关闭弹窗
const handleClosed = () => {
function handleClosed() {
emit('closed');
resetData();
};
// 弹窗配置
const [ModalComponent, modalApi] = useVbenModal({
title: props.title,
onCancel: handleCancel,
onClosed: handleClosed,
destroyOnClose: true,
});
}
// 递归处理部门树节点
const processDeptNode = (node: any): DeptTreeNode => {
function processDeptNode(node: any): DeptTreeNode {
return {
key: String(node.id),
title: `${node.name} (${node.id})`,
name: node.name,
children: node.children?.map((child: any) => processDeptNode(child)),
};
};
defineExpose({
open,
});
}
</script>
<template>
<ModalComponent class="w-[1000px]" key="user-select-modal">
<Spin :spinning="loading">
<Row :gutter="[16, 16]">
<Col :span="6">
<div class="h-[500px] overflow-auto rounded border">
<div class="border-b p-2">
<Input
v-model:value="deptSearchKeys"
placeholder="搜索部门"
allow-clear
@input="(e) => handleDeptSearch(e.target?.value ?? '')"
/>
</div>
<Tree
:tree-data="filteredDeptTree"
:expanded-keys="expandedKeys"
:selected-keys="selectedDeptId ? [String(selectedDeptId)] : []"
@select="handleDeptSelect"
@expand="handleExpand"
<Modal class="w-[40%]" key="user-select-modal" :title="title">
<Row :gutter="[16, 16]">
<Col :span="6">
<div class="h-[500px] overflow-auto rounded border">
<div class="border-b p-2">
<Input
v-model:value="deptSearchKeys"
placeholder="搜索部门"
allow-clear
@input="(e) => handleDeptSearch(e.target?.value ?? '')"
/>
</div>
</Col>
<Col :span="18">
<Transfer
:row-key="(record) => String(record.id)"
:data-source="transferDataSource"
v-model:target-keys="selectedUserIds"
:titles="['未选', '已选']"
:show-search="true"
:show-select-all="true"
:filter-option="filterOption"
@change="handleUserChange"
@search="handleUserSearch"
>
<template #render="item">
<span>{{ item?.nickname }} ({{ item?.username }})</span>
</template>
<Tree
:tree-data="filteredDeptTree"
:expanded-keys="expandedKeys"
:selected-keys="selectedDeptId ? [String(selectedDeptId)] : []"
@select="handleDeptSelect"
@expand="handleExpand"
/>
</div>
</Col>
<Col :span="18">
<Transfer
:row-key="(record) => String(record.id)"
:data-source="transferDataSource"
v-model:target-keys="selectedUserIds"
:titles="['未选', '已选']"
:show-search="true"
:show-select-all="true"
:filter-option="filterOption"
@change="handleUserChange"
@search="handleUserSearch"
>
<template #render="item">
<span>{{ item?.nickname }} ({{ item?.username }})</span>
</template>
<template #footer="{ direction }">
<div v-if="direction === 'left'">
<Pagination
v-model:current="leftListState.pagination.current"
v-model:page-size="leftListState.pagination.pageSize"
:total="leftListState.pagination.total"
:show-size-changer="true"
:show-total="(total) => `${total}`"
size="small"
@change="handleLeftPaginationChange"
/>
</div>
<template #footer="{ direction }">
<div v-if="direction === 'left'">
<Pagination
v-model:current="leftListState.pagination.current"
v-model:page-size="leftListState.pagination.pageSize"
:total="leftListState.pagination.total"
:show-size-changer="true"
:show-total="(total) => `${total}`"
size="small"
@change="handleLeftPaginationChange"
/>
</div>
<div v-if="direction === 'right'">
<Pagination
v-model:current="rightListState.pagination.current"
v-model:page-size="rightListState.pagination.pageSize"
:total="rightListState.pagination.total"
:show-size-changer="true"
:show-total="(total) => `${total}`"
size="small"
@change="handleRightPaginationChange"
/>
</div>
</template>
</Transfer>
</Col>
</Row>
</Spin>
<div v-if="direction === 'right'">
<Pagination
v-model:current="rightListState.pagination.current"
v-model:page-size="rightListState.pagination.pageSize"
:total="rightListState.pagination.total"
:show-size-changer="true"
:show-total="(total) => `${total}`"
size="small"
@change="handleRightPaginationChange"
/>
</div>
</template>
</Transfer>
</Col>
</Row>
<template #footer>
<Button
type="primary"
@@ -490,7 +483,7 @@ defineExpose({
</Button>
<Button @click="handleCancel">{{ cancelText }}</Button>
</template>
</ModalComponent>
</Modal>
</template>
<style lang="scss" scoped>

View File

@@ -1,7 +1,7 @@
<script setup lang="ts">
import type { SimpleFlowNode } from '../../consts';
import { ref, watch } from 'vue';
import { nextTick, ref, watch } from 'vue';
import { useVbenDrawer } from '@vben/common-ui';
import { IconifyIcon } from '@vben/icons';
@@ -53,8 +53,6 @@ const condition = ref<any>({
},
});
// 显示名称输入框
const showInput = ref(false);
const conditionRef = ref();
const fieldOptions = useFormFieldsAndStartUser(); // 流程表单字段和发起人字段
@@ -130,13 +128,24 @@ watch(
currentNode.value = newValue;
},
);
// 显示名称输入框
const showInput = ref(false);
// 输入框的引用
const inputRef = ref<HTMLInputElement | null>(null);
// 监听 showInput 的变化,当变为 true 时自动聚焦
watch(showInput, (value) => {
if (value) {
nextTick(() => {
inputRef.value?.focus();
});
}
});
function clickIcon() {
showInput.value = true;
}
// 输入框失去焦点
function blurEvent() {
// 修改节点名称
function changeNodeName() {
showInput.value = false;
currentNode.value.name =
currentNode.value.name ||
@@ -153,10 +162,12 @@ defineExpose({ open }); // 提供 open 方法,用于打开弹窗
<template #title>
<div class="flex items-center">
<Input
ref="inputRef"
v-if="showInput"
type="text"
class="mr-2 w-48"
@blur="blurEvent()"
@blur="changeNodeName()"
@press-enter="changeNodeName()"
v-model:value="currentNode.name"
:placeholder="currentNode.name"
/>
@@ -166,7 +177,7 @@ defineExpose({ open }); // 提供 open 方法,用于打开弹窗
@click="clickIcon()"
>
{{ currentNode.name }}
<IconifyIcon class="ml-1" icon="ep:edit-pen" />
<IconifyIcon class="ml-1" icon="lucide:edit-3" />
</div>
</div>
</template>

View File

@@ -27,14 +27,13 @@ import {
TreeSelect,
} from 'ant-design-vue';
import { BpmModelFormType } from '#/utils';
import { BpmModelFormType, BpmNodeTypeEnum } from '#/utils';
import {
CANDIDATE_STRATEGY,
CandidateStrategy,
FieldPermissionType,
MULTI_LEVEL_DEPT,
NodeType,
} from '../../consts';
import {
useFormFieldsPermission,
@@ -76,9 +75,8 @@ const [Drawer, drawerApi] = useVbenDrawer({
const currentNode = useWatchNode(props);
// 节点名称
const { nodeName, showInput, clickIcon, blurEvent } = useNodeName(
NodeType.COPY_TASK_NODE,
);
const { nodeName, showInput, clickIcon, changeNodeName, inputRef } =
useNodeName(BpmNodeTypeEnum.COPY_TASK_NODE);
// 激活的 Tab 标签页
const activeTabName = ref('user');
@@ -137,7 +135,7 @@ const {
getShowText,
handleCandidateParam,
parseCandidateParam,
} = useNodeForm(NodeType.COPY_TASK_NODE);
} = useNodeForm(BpmNodeTypeEnum.COPY_TASK_NODE);
const configForm = tempConfigForm as Ref<CopyTaskFormType>;
// 抄送人策略, 去掉发起人自选 和 发起人自己
@@ -214,15 +212,17 @@ defineExpose({ showCopyTaskNodeConfig }); // 暴露方法给父组件
<div class="config-header">
<Input
v-if="showInput"
ref="inputRef"
type="text"
class="config-editable-input"
@blur="blurEvent()"
@blur="changeNodeName()"
@press-enter="changeNodeName()"
v-model:value="nodeName"
:placeholder="nodeName"
/>
<div v-else class="node-name">
{{ nodeName }}
<IconifyIcon class="ml-1" icon="ep:edit-pen" @click="clickIcon()" />
<IconifyIcon class="ml-1" icon="lucide:edit-3" @click="clickIcon()" />
</div>
</div>
</template>

View File

@@ -22,10 +22,11 @@ import {
SelectOption,
} from 'ant-design-vue';
import { BpmNodeTypeEnum } from '#/utils';
import {
DELAY_TYPE,
DelayTypeEnum,
NodeType,
TIME_UNIT_TYPES,
TimeUnitType,
} from '../../consts';
@@ -44,9 +45,8 @@ const props = defineProps({
// 当前节点
const currentNode = useWatchNode(props);
// 节点名称
const { nodeName, showInput, clickIcon, blurEvent } = useNodeName(
NodeType.DELAY_TIMER_NODE,
);
const { nodeName, showInput, clickIcon, changeNodeName, inputRef } =
useNodeName(BpmNodeTypeEnum.DELAY_TIMER_NODE);
// 抄送人表单配置
const formRef = ref(); // 表单 Ref
@@ -157,9 +157,11 @@ defineExpose({ openDrawer }); // 暴露方法给父组件
<div class="flex items-center">
<Input
v-if="showInput"
ref="inputRef"
type="text"
class="mr-2 w-48"
@blur="blurEvent()"
@blur="changeNodeName()"
@press-enter="changeNodeName()"
v-model:value="nodeName"
:placeholder="nodeName"
/>
@@ -169,7 +171,7 @@ defineExpose({ openDrawer }); // 暴露方法给父组件
@click="clickIcon()"
>
{{ nodeName }}
<IconifyIcon class="ml-1" icon="ep:edit-pen" :size="16" />
<IconifyIcon class="ml-1" icon="lucide:edit-3" :size="16" />
</div>
</div>
</template>

View File

@@ -33,6 +33,18 @@ const [Modal, modalApi] = useVbenModal({
title: '条件配置',
destroyOnClose: true,
draggable: true,
onOpenChange(isOpen) {
if (isOpen) {
// 获取传递的数据
const conditionObj = modalApi.getData();
if (conditionObj) {
conditionData.value.conditionType = conditionObj.conditionType;
conditionData.value.conditionExpression =
conditionObj.conditionExpression;
conditionData.value.conditionGroups = conditionObj.conditionGroups;
}
}
},
async onConfirm() {
// 校验表单
if (!conditionRef.value) return;
@@ -50,17 +62,8 @@ const [Modal, modalApi] = useVbenModal({
},
});
// TODO: jason open 在 useVbenModal 中 onOpenChange 方法
function open(conditionObj: any | undefined) {
if (conditionObj) {
conditionData.value.conditionType = conditionObj.conditionType;
conditionData.value.conditionExpression = conditionObj.conditionExpression;
conditionData.value.conditionGroups = conditionObj.conditionGroups;
}
modalApi.open();
}
// TODO: jason 不需要暴露expose直接使用modalApi.setData(formSetting).open()
defineExpose({ open });
// TODO xingyu 暴露 modalApi 给父组件是否合适? trigger-node-config.vue 会有多个 conditionDialog 实例
defineExpose({ modalApi });
</script>
<template>
<Modal class="w-1/2">

View File

@@ -25,7 +25,7 @@ import {
Tooltip,
} from 'ant-design-vue';
import { BpmModelFormType } from '#/utils/constants';
import { BpmModelFormType } from '#/utils';
import {
COMPARISON_OPERATORS,
@@ -188,7 +188,7 @@ defineExpose({ validate });
>
<IconifyIcon
color="#0089ff"
icon="ep:circle-close-filled"
icon="lucide:circle-x"
class="size-4"
@click="
deleteConditionGroup(condition.conditionGroups.conditions, cIdx)

View File

@@ -21,7 +21,9 @@ import {
SelectOption,
} from 'ant-design-vue';
import { ConditionType, NodeType } from '../../consts';
import { BpmNodeTypeEnum } from '#/utils';
import { ConditionType } from '../../consts';
import { useNodeName, useWatchNode } from '../../helpers';
import Condition from './modules/condition.vue';
@@ -39,9 +41,8 @@ const processNodeTree = inject<Ref<SimpleFlowNode>>('processNodeTree');
/** 当前节点 */
const currentNode = useWatchNode(props);
/** 节点名称 */
const { nodeName, showInput, clickIcon, blurEvent } = useNodeName(
NodeType.ROUTER_BRANCH_NODE,
);
const { nodeName, showInput, clickIcon, changeNodeName, inputRef } =
useNodeName(BpmNodeTypeEnum.ROUTER_BRANCH_NODE);
const routerGroups = ref<RouterSetting[]>([]);
const nodeOptions = ref<any[]>([]);
const conditionRef = ref<any[]>([]);
@@ -176,15 +177,15 @@ function getRouterNode(node: any) {
while (true) {
if (!node) break;
if (
node.type !== NodeType.ROUTER_BRANCH_NODE &&
node.type !== NodeType.CONDITION_NODE
node.type !== BpmNodeTypeEnum.ROUTER_BRANCH_NODE &&
node.type !== BpmNodeTypeEnum.CONDITION_NODE
) {
nodeOptions.value.push({
label: node.name,
value: node.id,
});
}
if (!node.childNode || node.type === NodeType.END_EVENT_NODE) {
if (!node.childNode || node.type === BpmNodeTypeEnum.END_EVENT_NODE) {
break;
}
if (node.conditionNodes && node.conditionNodes.length > 0) {
@@ -199,14 +200,16 @@ function getRouterNode(node: any) {
defineExpose({ openDrawer }); // 暴露方法给父组件
</script>
<template>
<Drawer class="w-[630px]">
<Drawer class="w-[40%]">
<template #title>
<div class="flex items-center">
<Input
ref="inputRef"
v-if="showInput"
type="text"
class="mr-2 w-48"
@blur="blurEvent()"
@blur="changeNodeName()"
@press-enter="changeNodeName()"
v-model:value="nodeName"
:placeholder="nodeName"
/>
@@ -216,7 +219,7 @@ defineExpose({ openDrawer }); // 暴露方法给父组件
@click="clickIcon()"
>
{{ nodeName }}
<IconifyIcon class="ml-1" icon="ep:edit-pen" />
<IconifyIcon class="ml-1" icon="lucide:edit-3" />
</div>
</div>
</template>
@@ -263,7 +266,7 @@ defineExpose({ openDrawer }); // 暴露方法给父组件
@click="deleteRouterGroup(index)"
>
<template #icon>
<IconifyIcon icon="ep:close" />
<IconifyIcon icon="lucide:x" />
</template>
</Button>
</div>
@@ -284,7 +287,7 @@ defineExpose({ openDrawer }); // 暴露方法给父组件
@click="addRouterGroup"
>
<template #icon>
<IconifyIcon icon="ep:setting" />
<IconifyIcon icon="lucide:settings" />
</template>
新增路由分支
</Button>

View File

@@ -23,13 +23,9 @@ import {
TypographyText,
} from 'ant-design-vue';
import { BpmModelFormType } from '#/utils';
import { BpmModelFormType, BpmNodeTypeEnum } from '#/utils';
import {
FieldPermissionType,
NodeType,
START_USER_BUTTON_SETTING,
} from '../../consts';
import { FieldPermissionType, START_USER_BUTTON_SETTING } from '../../consts';
import {
useFormFieldsPermission,
useNodeName,
@@ -56,9 +52,8 @@ const deptOptions = inject<Ref<SystemDeptApi.Dept[]>>('deptList');
// 当前节点
const currentNode = useWatchNode(props);
// 节点名称
const { nodeName, showInput, clickIcon, blurEvent } = useNodeName(
NodeType.COPY_TASK_NODE,
);
const { nodeName, showInput, clickIcon, changeNodeName, inputRef } =
useNodeName(BpmNodeTypeEnum.START_USER_NODE);
// 激活的 Tab 标签页
const activeTabName = ref('user');
@@ -149,12 +144,13 @@ defineExpose({ showStartUserNodeConfig });
<Drawer>
<template #title>
<div class="config-header">
<!-- TODO v-mountedFocus 自动聚集 需要迁移一下 -->
<Input
ref="inputRef"
v-if="showInput"
type="text"
class="config-editable-input"
@blur="blurEvent()"
@blur="changeNodeName()"
@press-enter="changeNodeName()"
v-model:value="nodeName"
:placeholder="nodeName"
/>
@@ -162,7 +158,7 @@ defineExpose({ showStartUserNodeConfig });
{{ nodeName }}
<IconifyIcon
class="ml-1"
icon="ep:edit-pen"
icon="lucide:edit-3"
:size="16"
@click="clickIcon()"
/>

View File

@@ -29,9 +29,10 @@ import {
Tag,
} from 'ant-design-vue';
import { BpmNodeTypeEnum } from '#/utils';
import {
DEFAULT_CONDITION_GROUP_VALUE,
NodeType,
TRIGGER_TYPES,
TriggerTypeEnum,
} from '../../consts';
@@ -71,9 +72,8 @@ const [Drawer, drawerApi] = useVbenDrawer({
// 当前节点
const currentNode = useWatchNode(props);
// 节点名称
const { nodeName, showInput, clickIcon, blurEvent } = useNodeName(
NodeType.TRIGGER_NODE,
);
const { nodeName, showInput, clickIcon, changeNodeName, inputRef } =
useNodeName(BpmNodeTypeEnum.TRIGGER_NODE);
// 触发器表单配置
const formRef = ref(); // 表单 Ref
@@ -200,8 +200,8 @@ function addFormSettingCondition(
formSetting: FormTriggerSetting,
) {
const conditionDialog = proxy.$refs[`condition-${index}`][0];
// TODO: jason Modal 使用 useVbenModal 初始化弹出使用modalApi.setData(formSetting).open()
conditionDialog.open(formSetting);
// 使用modalApi来打开模态框并传递数据
conditionDialog.modalApi.setData(formSetting).open();
}
/** 删除条件配置 */
@@ -215,7 +215,8 @@ function openFormSettingCondition(
formSetting: FormTriggerSetting,
) {
const conditionDialog = proxy.$refs[`condition-${index}`][0];
conditionDialog.open(formSetting);
// 使用 modalApi 来打开模态框并传递数据
conditionDialog.modalApi.setData(formSetting).open();
}
/** 处理条件配置保存 */
@@ -387,16 +388,18 @@ onMounted(() => {
<template #title>
<div class="config-header">
<Input
ref="inputRef"
v-if="showInput"
type="text"
class="config-editable-input"
@blur="blurEvent()"
@blur="changeNodeName()"
@press-enter="changeNodeName()"
v-model:value="nodeName"
:placeholder="nodeName"
/>
<div v-else class="node-name">
{{ nodeName }}
<IconifyIcon class="ml-1" icon="ep:edit-pen" @click="clickIcon()" />
<IconifyIcon class="ml-1" icon="lucide:edit-3" @click="clickIcon()" />
</div>
</div>
</template>
@@ -453,7 +456,7 @@ onMounted(() => {
@click="deleteFormSetting(index)"
>
<template #icon>
<IconifyIcon icon="ep:close" />
<IconifyIcon icon="lucide:x" />
</template>
</Button>
</div>
@@ -483,7 +486,7 @@ onMounted(() => {
@click="addFormSettingCondition(index, formSetting)"
>
<template #icon>
<IconifyIcon icon="ep:link" />
<IconifyIcon icon="lucide:link" />
</template>
添加条件
</Button>
@@ -558,7 +561,7 @@ onMounted(() => {
@click="addFormFieldSetting(formSetting)"
>
<template #icon>
<IconifyIcon icon="ep:memo" />
<IconifyIcon icon="lucide:file-cog" />
</template>
添加修改字段
</Button>
@@ -576,7 +579,7 @@ onMounted(() => {
@click="addFormSetting"
>
<template #icon>
<IconifyIcon icon="ep:setting" />
<IconifyIcon icon="lucide:settings" />
</template>
添加设置
</Button>
@@ -601,7 +604,7 @@ onMounted(() => {
@click="deleteFormSetting(index)"
>
<template #icon>
<IconifyIcon icon="ep:close" />
<IconifyIcon icon="lucide:x" />
</template>
</Button>
</div>
@@ -632,7 +635,7 @@ onMounted(() => {
@click="addFormSettingCondition(index, formSetting)"
>
<template #icon>
<IconifyIcon icon="ep:link" />
<IconifyIcon icon="lucide:link" />
</template>
添加条件
</Button>
@@ -670,7 +673,7 @@ onMounted(() => {
@click="addFormSetting"
>
<template #icon>
<IconifyIcon icon="ep:setting" />
<IconifyIcon icon="lucide:settings" />
</template>
添加设置
</Button>

View File

@@ -34,7 +34,11 @@ import {
TypographyText,
} from 'ant-design-vue';
import { BpmModelFormType } from '#/utils';
import {
BpmModelFormType,
BpmNodeTypeEnum,
ProcessVariableEnum,
} from '#/utils';
import {
APPROVE_METHODS,
@@ -49,9 +53,7 @@ import {
DEFAULT_BUTTON_SETTING,
FieldPermissionType,
MULTI_LEVEL_DEPT,
NodeType,
OPERATION_BUTTON_NAME,
ProcessVariableEnum,
REJECT_HANDLER_TYPES,
RejectHandlerType,
TIME_UNIT_TYPES,
@@ -112,9 +114,8 @@ const [Drawer, drawerApi] = useVbenDrawer({
});
// 节点名称配置
const { nodeName, showInput, clickIcon, blurEvent } = useNodeName(
NodeType.USER_TASK_NODE,
);
const { nodeName, showInput, clickIcon, changeNodeName, inputRef } =
useNodeName(BpmNodeTypeEnum.USER_TASK_NODE);
// 激活的 Tab 标签页
const activeTabName = ref('user');
@@ -245,7 +246,9 @@ const userTaskListenerRef = ref();
/** 节点类型名称 */
const nodeTypeName = computed(() => {
return currentNode.value.type === NodeType.TRANSACTOR_NODE ? '办理' : '审批';
return currentNode.value.type === BpmNodeTypeEnum.TRANSACTOR_NODE
? '办理'
: '审批';
});
/** 校验节点配置 */
@@ -407,7 +410,7 @@ function showUserTaskNodeConfig(node: SimpleFlowNode) {
// 3. 操作按钮设置
buttonsSetting.value =
cloneDeep(node.buttonsSetting) ||
(node.type === NodeType.TRANSACTOR_NODE
(node.type === BpmNodeTypeEnum.TRANSACTOR_NODE
? TRANSACTOR_DEFAULT_BUTTON_SETTING
: DEFAULT_BUTTON_SETTING);
// 4. 表单字段权限配置
@@ -582,20 +585,22 @@ onMounted(() => {
<div class="config-header">
<Input
v-if="showInput"
ref="inputRef"
type="text"
class="config-editable-input"
@blur="blurEvent()"
@blur="changeNodeName()"
@press-enter="changeNodeName()"
v-model:value="nodeName"
:placeholder="nodeName"
/>
<div v-else class="node-name">
{{ nodeName }}
<IconifyIcon class="ml-1" icon="ep:edit-pen" @click="clickIcon()" />
<IconifyIcon class="ml-1" icon="lucide:edit-3" @click="clickIcon()" />
</div>
</div>
</template>
<div
v-if="currentNode.type === NodeType.USER_TASK_NODE"
v-if="currentNode.type === BpmNodeTypeEnum.USER_TASK_NODE"
class="mb-3 flex items-center"
>
<span class="mr-3 text-[16px]">审批类型 :</span>
@@ -860,7 +865,7 @@ onMounted(() => {
</RadioGroup>
</FormItem>
<div v-if="currentNode.type === NodeType.USER_TASK_NODE">
<div v-if="currentNode.type === BpmNodeTypeEnum.USER_TASK_NODE">
<Divider content-position="left">审批人拒绝时</Divider>
<FormItem name="rejectHandlerType">
<RadioGroup
@@ -902,7 +907,7 @@ onMounted(() => {
</FormItem>
</div>
<div v-if="currentNode.type === NodeType.USER_TASK_NODE">
<div v-if="currentNode.type === BpmNodeTypeEnum.USER_TASK_NODE">
<Divider content-position="left">审批人超时未处理时</Divider>
<FormItem
label="启用开关"
@@ -1047,7 +1052,7 @@ onMounted(() => {
</Select>
</FormItem>
<div v-if="currentNode.type === NodeType.USER_TASK_NODE">
<div v-if="currentNode.type === BpmNodeTypeEnum.USER_TASK_NODE">
<Divider content-position="left">
审批人与提交人为同一人时
</Divider>
@@ -1070,7 +1075,7 @@ onMounted(() => {
</FormItem>
</div>
<div v-if="currentNode.type === NodeType.USER_TASK_NODE">
<div v-if="currentNode.type === BpmNodeTypeEnum.USER_TASK_NODE">
<Divider content-position="left">是否需要签名</Divider>
<FormItem name="signEnable">
<Switch
@@ -1081,7 +1086,7 @@ onMounted(() => {
</FormItem>
</div>
<div v-if="currentNode.type === NodeType.USER_TASK_NODE">
<div v-if="currentNode.type === BpmNodeTypeEnum.USER_TASK_NODE">
<Divider content-position="left">审批意见</Divider>
<FormItem name="reasonRequire">
<Switch
@@ -1096,7 +1101,7 @@ onMounted(() => {
</TabPane>
<TabPane
tab="操作按钮设置"
v-if="currentNode.type === NodeType.USER_TASK_NODE"
v-if="currentNode.type === BpmNodeTypeEnum.USER_TASK_NODE"
key="buttons"
>
<div class="p-1">
@@ -1130,7 +1135,7 @@ onMounted(() => {
<Button v-else text @click="changeBtnDisplayName(index)">
<div class="flex items-center">
{{ item.displayName }}
<IconifyIcon icon="ep:edit" class="ml-2" />
<IconifyIcon icon="lucide:edit" class="ml-2" />
</div>
</Button>
</Col>

View File

@@ -7,7 +7,9 @@ import { IconifyIcon } from '@vben/icons';
import { Input } from 'ant-design-vue';
import { NODE_DEFAULT_TEXT, NodeType } from '../../consts';
import { BpmNodeTypeEnum } from '#/utils';
import { NODE_DEFAULT_TEXT } from '../../consts';
import { useNodeName2, useTaskStatusClass, useWatchNode } from '../../helpers';
import CopyTaskNodeConfig from '../nodes-config/copy-task-node-config.vue';
import NodeHandler from './node-handler.vue';
@@ -30,9 +32,9 @@ const readonly = inject<Boolean>('readonly');
// 监控节点的变化
const currentNode = useWatchNode(props);
// 节点名称编辑
const { showInput, blurEvent, clickTitle } = useNodeName2(
const { showInput, changeNodeName, clickTitle, inputRef } = useNodeName2(
currentNode,
NodeType.COPY_TASK_NODE,
BpmNodeTypeEnum.COPY_TASK_NODE,
);
const nodeSetting = ref();
@@ -65,11 +67,13 @@ function deleteNode() {
<span class="iconfont icon-copy"></span>
</div>
<Input
ref="inputRef"
v-if="!readonly && showInput"
type="text"
class="editable-title-input"
@blur="blurEvent()"
v-model="currentNode.name"
@blur="changeNodeName()"
@press-enter="changeNodeName()"
v-model:value="currentNode.name"
:placeholder="currentNode.name"
/>
<div v-else class="node-title" @click="clickTitle">
@@ -85,15 +89,15 @@ function deleteNode() {
{{ currentNode.showText }}
</div>
<div class="node-text" v-else>
{{ NODE_DEFAULT_TEXT.get(NodeType.COPY_TASK_NODE) }}
{{ NODE_DEFAULT_TEXT.get(BpmNodeTypeEnum.COPY_TASK_NODE) }}
</div>
<IconifyIcon v-if="!readonly" icon="ep:arrow-right-bold" />
<IconifyIcon v-if="!readonly" icon="lucide:chevron-right" />
</div>
<div v-if="!readonly" class="node-toolbar">
<div class="toolbar-icon">
<IconifyIcon
color="#0089ff"
icon="ep:circle-close-filled"
icon="lucide:circle-x"
:size="18"
@click="deleteNode"
/>

View File

@@ -7,7 +7,9 @@ import { IconifyIcon } from '@vben/icons';
import { Input } from 'ant-design-vue';
import { NODE_DEFAULT_TEXT, NodeType } from '../../consts';
import { BpmNodeTypeEnum } from '#/utils';
import { NODE_DEFAULT_TEXT } from '../../consts';
import { useNodeName2, useTaskStatusClass, useWatchNode } from '../../helpers';
import DelayTimerNodeConfig from '../nodes-config/delay-timer-node-config.vue';
import NodeHandler from './node-handler.vue';
@@ -28,9 +30,9 @@ const readonly = inject<Boolean>('readonly');
// 监控节点的变化
const currentNode = useWatchNode(props);
// 节点名称编辑
const { showInput, blurEvent, clickTitle } = useNodeName2(
const { showInput, changeNodeName, clickTitle, inputRef } = useNodeName2(
currentNode,
NodeType.DELAY_TIMER_NODE,
BpmNodeTypeEnum.DELAY_TIMER_NODE,
);
const nodeSetting = ref();
@@ -62,11 +64,13 @@ function deleteNode() {
<span class="iconfont icon-delay"></span>
</div>
<Input
ref="inputRef"
v-if="!readonly && showInput"
type="text"
class="editable-title-input"
@blur="blurEvent()"
v-model="currentNode.name"
@blur="changeNodeName()"
@press-enter="changeNodeName()"
v-model:value="currentNode.name"
:placeholder="currentNode.name"
/>
<div v-else class="node-title" @click="clickTitle">
@@ -82,15 +86,15 @@ function deleteNode() {
{{ currentNode.showText }}
</div>
<div class="node-text" v-else>
{{ NODE_DEFAULT_TEXT.get(NodeType.DELAY_TIMER_NODE) }}
{{ NODE_DEFAULT_TEXT.get(BpmNodeTypeEnum.DELAY_TIMER_NODE) }}
</div>
<IconifyIcon v-if="!readonly" icon="ep:arrow-right-bold" />
<IconifyIcon v-if="!readonly" icon="lucide:chevron-right" />
</div>
<div v-if="!readonly" class="node-toolbar">
<div class="toolbar-icon">
<IconifyIcon
color="#0089ff"
icon="ep:circle-close-filled"
icon="lucide:circle-x"
:size="18"
@click="deleteNode"
/>

View File

@@ -1,18 +1,19 @@
<script setup lang="ts">
import type { SimpleFlowNode } from '../../consts';
import { getCurrentInstance, inject, ref, watch } from 'vue';
import { getCurrentInstance, inject, nextTick, ref, watch } from 'vue';
import { IconifyIcon } from '@vben/icons';
import { cloneDeep, buildShortUUID as generateUUID } from '@vben/utils';
import { Button, Input } from 'ant-design-vue';
import { BpmNodeTypeEnum } from '#/utils';
import {
ConditionType,
DEFAULT_CONDITION_GROUP_VALUE,
NODE_DEFAULT_TEXT,
NodeType,
} from '../../consts';
import { getDefaultConditionNodeName, useTaskStatusClass } from '../../helpers';
import ConditionNodeConfig from '../nodes-config/condition-node-config.vue';
@@ -50,11 +51,30 @@ watch(
currentNode.value = newValue;
},
);
// 条件节点名称输入框引用
const inputRefs = ref<HTMLInputElement[]>([]);
// 节点名称输入框显示状态
const showInputs = ref<boolean[]>([]);
// 失去焦点
function blurEvent(index: number) {
// 监听显示状态变化
watch(
showInputs,
(newValues) => {
// 当状态为 true 时, 自动聚焦
newValues.forEach((value, index) => {
if (value) {
// 当显示状态从 false 变为 true 时, 自动聚焦
nextTick(() => {
inputRefs.value[index]?.focus();
});
}
});
},
{ deep: true },
);
// 修改节点名称
function changeNodeName(index: number) {
showInputs.value[index] = false;
const conditionNode = currentNode.value.conditionNodes?.at(
index,
@@ -90,7 +110,7 @@ function addCondition() {
id: `Flow_${generateUUID()}`,
name: `条件${len}`,
showText: '',
type: NodeType.CONDITION_NODE,
type: BpmNodeTypeEnum.CONDITION_NODE,
childNode: undefined,
conditionNodes: [],
conditionSetting: {
@@ -138,7 +158,7 @@ function recursiveFindParentNode(
node: SimpleFlowNode,
nodeType: number,
) {
if (!node || node.type === NodeType.START_USER_NODE) {
if (!node || node.type === BpmNodeTypeEnum.START_USER_NODE) {
return;
}
if (node.type === nodeType) {
@@ -187,10 +207,16 @@ function recursiveFindParentNode(
<div class="branch-node-title-container">
<div v-if="!readonly && showInputs[index]">
<Input
:ref="
(el) => {
inputRefs[index] = el as HTMLInputElement;
}
"
type="text"
class="editable-title-input"
@blur="blurEvent(index)"
v-model="item.name"
@blur="changeNodeName(index)"
@press-enter="changeNodeName(index)"
v-model:value="item.name"
/>
</div>
<div v-else class="branch-title" @click="clickEvent(index)">
@@ -210,7 +236,7 @@ function recursiveFindParentNode(
{{ item.showText }}
</div>
<div class="branch-node-text" v-else>
{{ NODE_DEFAULT_TEXT.get(NodeType.CONDITION_NODE) }}
{{ NODE_DEFAULT_TEXT.get(BpmNodeTypeEnum.CONDITION_NODE) }}
</div>
</div>
<div
@@ -222,7 +248,7 @@ function recursiveFindParentNode(
<div class="toolbar-icon">
<IconifyIcon
color="#0089ff"
icon="ep:circle-close-filled"
icon="lucide:circle-x"
:size="18"
@click="deleteCondition(index)"
/>
@@ -237,7 +263,7 @@ function recursiveFindParentNode(
"
@click="moveNode(index, -1)"
>
<IconifyIcon icon="ep:arrow-left" />
<IconifyIcon icon="lucide:chevron-left" />
</div>
<div
@@ -249,7 +275,7 @@ function recursiveFindParentNode(
"
@click="moveNode(index, 1)"
>
<IconifyIcon icon="ep:arrow-right" />
<IconifyIcon icon="lucide:chevron-right" />
</div>
</div>
<NodeHandler

View File

@@ -1,18 +1,19 @@
<script setup lang="ts">
import type { SimpleFlowNode } from '../../consts';
import { getCurrentInstance, inject, ref, watch } from 'vue';
import { getCurrentInstance, inject, nextTick, ref, watch } from 'vue';
import { IconifyIcon } from '@vben/icons';
import { cloneDeep, buildShortUUID as generateUUID } from '@vben/utils';
import { Button, Input } from 'ant-design-vue';
import { BpmNodeTypeEnum } from '#/utils';
import {
ConditionType,
DEFAULT_CONDITION_GROUP_VALUE,
NODE_DEFAULT_TEXT,
NodeType,
} from '../../consts';
import {
getDefaultInclusiveConditionNodeName,
@@ -56,10 +57,28 @@ watch(
currentNode.value = newValue;
},
);
// 条件节点名称输入框引用
const inputRefs = ref<HTMLInputElement[]>([]);
// 节点名称输入框显示状态
const showInputs = ref<boolean[]>([]);
// 失去焦点
function blurEvent(index: number) {
// 监听显示状态变化
watch(
showInputs,
(newValues) => {
// 当状态为 true 时, 自动聚焦
newValues.forEach((value, index) => {
if (value) {
// 当显示状态从 false 变为 true 时, 自动聚焦
nextTick(() => {
inputRefs.value[index]?.focus();
});
}
});
},
{ deep: true },
);
// 修改节点名称
function changeNodeName(index: number) {
showInputs.value[index] = false;
const conditionNode = currentNode.value.conditionNodes?.at(
index,
@@ -95,7 +114,7 @@ function addCondition() {
id: `Flow_${generateUUID()}`,
name: `包容条件${len}`,
showText: '',
type: NodeType.CONDITION_NODE,
type: BpmNodeTypeEnum.CONDITION_NODE,
childNode: undefined,
conditionNodes: [],
conditionSetting: {
@@ -143,7 +162,7 @@ function recursiveFindParentNode(
node: SimpleFlowNode,
nodeType: number,
) {
if (!node || node.type === NodeType.START_USER_NODE) {
if (!node || node.type === BpmNodeTypeEnum.START_USER_NODE) {
return;
}
if (node.type === nodeType) {
@@ -191,10 +210,16 @@ function recursiveFindParentNode(
<div class="branch-node-title-container">
<div v-if="!readonly && showInputs[index]">
<Input
:ref="
(el) => {
inputRefs[index] = el as HTMLInputElement;
}
"
type="text"
class="editable-title-input"
@blur="blurEvent(index)"
v-model="item.name"
@blur="changeNodeName(index)"
@press-enter="changeNodeName(index)"
v-model:value="item.name"
/>
</div>
<div v-else class="branch-title" @click="clickEvent(index)">
@@ -213,7 +238,7 @@ function recursiveFindParentNode(
{{ item.showText }}
</div>
<div class="branch-node-text" v-else>
{{ NODE_DEFAULT_TEXT.get(NodeType.CONDITION_NODE) }}
{{ NODE_DEFAULT_TEXT.get(BpmNodeTypeEnum.CONDITION_NODE) }}
</div>
</div>
<div
@@ -225,7 +250,7 @@ function recursiveFindParentNode(
<div class="toolbar-icon">
<IconifyIcon
color="#0089ff"
icon="ep:circle-close-filled"
icon="lucide:circle-x"
:size="18"
@click="deleteCondition(index)"
/>
@@ -240,7 +265,7 @@ function recursiveFindParentNode(
"
@click="moveNode(index, -1)"
>
<IconifyIcon icon="ep:arrow-left" />
<IconifyIcon icon="lucide:chevron-left" />
</div>
<div
@@ -252,7 +277,7 @@ function recursiveFindParentNode(
"
@click="moveNode(index, 1)"
>
<IconifyIcon icon="ep:arrow-right" />
<IconifyIcon icon="lucide:chevron-right" />
</div>
</div>
<NodeHandler

View File

@@ -8,6 +8,8 @@ import { cloneDeep, buildShortUUID as generateUUID } from '@vben/utils';
import { message, Popover } from 'ant-design-vue';
import { BpmNodeTypeEnum } from '#/utils';
import {
ApproveMethodType,
AssignEmptyHandlerType,
@@ -15,7 +17,6 @@ import {
ConditionType,
DEFAULT_CONDITION_GROUP_VALUE,
NODE_DEFAULT_NAME,
NodeType,
RejectHandlerType,
} from '../../consts';
@@ -41,17 +42,21 @@ const readonly = inject<Boolean>('readonly'); // 是否只读
function addNode(type: number) {
// 校验:条件分支、包容分支后面,不允许直接添加并行分支
if (
type === NodeType.PARALLEL_BRANCH_NODE &&
[NodeType.CONDITION_BRANCH_NODE, NodeType.INCLUSIVE_BRANCH_NODE].includes(
props.currentNode?.type,
)
type === BpmNodeTypeEnum.PARALLEL_BRANCH_NODE &&
[
BpmNodeTypeEnum.CONDITION_BRANCH_NODE,
BpmNodeTypeEnum.INCLUSIVE_BRANCH_NODE,
].includes(props.currentNode?.type)
) {
message.error('条件分支、包容分支后面,不允许直接添加并行分支');
return;
}
popoverShow.value = false;
if (type === NodeType.USER_TASK_NODE || type === NodeType.TRANSACTOR_NODE) {
if (
type === BpmNodeTypeEnum.USER_TASK_NODE ||
type === BpmNodeTypeEnum.TRANSACTOR_NODE
) {
const id = `Activity_${generateUUID()}`;
const data: SimpleFlowNode = {
id,
@@ -83,20 +88,20 @@ function addNode(type: number) {
};
emits('update:childNode', data);
}
if (type === NodeType.COPY_TASK_NODE) {
if (type === BpmNodeTypeEnum.COPY_TASK_NODE) {
const data: SimpleFlowNode = {
id: `Activity_${generateUUID()}`,
name: NODE_DEFAULT_NAME.get(NodeType.COPY_TASK_NODE) as string,
name: NODE_DEFAULT_NAME.get(BpmNodeTypeEnum.COPY_TASK_NODE) as string,
showText: '',
type: NodeType.COPY_TASK_NODE,
type: BpmNodeTypeEnum.COPY_TASK_NODE,
childNode: props.childNode,
};
emits('update:childNode', data);
}
if (type === NodeType.CONDITION_BRANCH_NODE) {
if (type === BpmNodeTypeEnum.CONDITION_BRANCH_NODE) {
const data: SimpleFlowNode = {
name: '条件分支',
type: NodeType.CONDITION_BRANCH_NODE,
type: BpmNodeTypeEnum.CONDITION_BRANCH_NODE,
id: `GateWay_${generateUUID()}`,
childNode: props.childNode,
conditionNodes: [
@@ -104,7 +109,7 @@ function addNode(type: number) {
id: `Flow_${generateUUID()}`,
name: '条件1',
showText: '',
type: NodeType.CONDITION_NODE,
type: BpmNodeTypeEnum.CONDITION_NODE,
childNode: undefined,
conditionSetting: {
defaultFlow: false,
@@ -116,7 +121,7 @@ function addNode(type: number) {
id: `Flow_${generateUUID()}`,
name: '其它情况',
showText: '未满足其它条件时,将进入此分支',
type: NodeType.CONDITION_NODE,
type: BpmNodeTypeEnum.CONDITION_NODE,
childNode: undefined,
conditionSetting: {
defaultFlow: true,
@@ -126,10 +131,10 @@ function addNode(type: number) {
};
emits('update:childNode', data);
}
if (type === NodeType.PARALLEL_BRANCH_NODE) {
if (type === BpmNodeTypeEnum.PARALLEL_BRANCH_NODE) {
const data: SimpleFlowNode = {
name: '并行分支',
type: NodeType.PARALLEL_BRANCH_NODE,
type: BpmNodeTypeEnum.PARALLEL_BRANCH_NODE,
id: `GateWay_${generateUUID()}`,
childNode: props.childNode,
conditionNodes: [
@@ -137,24 +142,24 @@ function addNode(type: number) {
id: `Flow_${generateUUID()}`,
name: '并行1',
showText: '无需配置条件同时执行',
type: NodeType.CONDITION_NODE,
type: BpmNodeTypeEnum.CONDITION_NODE,
childNode: undefined,
},
{
id: `Flow_${generateUUID()}`,
name: '并行2',
showText: '无需配置条件同时执行',
type: NodeType.CONDITION_NODE,
type: BpmNodeTypeEnum.CONDITION_NODE,
childNode: undefined,
},
],
};
emits('update:childNode', data);
}
if (type === NodeType.INCLUSIVE_BRANCH_NODE) {
if (type === BpmNodeTypeEnum.INCLUSIVE_BRANCH_NODE) {
const data: SimpleFlowNode = {
name: '包容分支',
type: NodeType.INCLUSIVE_BRANCH_NODE,
type: BpmNodeTypeEnum.INCLUSIVE_BRANCH_NODE,
id: `GateWay_${generateUUID()}`,
childNode: props.childNode,
conditionNodes: [
@@ -162,7 +167,7 @@ function addNode(type: number) {
id: `Flow_${generateUUID()}`,
name: '包容条件1',
showText: '',
type: NodeType.CONDITION_NODE,
type: BpmNodeTypeEnum.CONDITION_NODE,
childNode: undefined,
conditionSetting: {
defaultFlow: false,
@@ -174,7 +179,7 @@ function addNode(type: number) {
id: `Flow_${generateUUID()}`,
name: '其它情况',
showText: '未满足其它条件时,将进入此分支',
type: NodeType.CONDITION_NODE,
type: BpmNodeTypeEnum.CONDITION_NODE,
childNode: undefined,
conditionSetting: {
defaultFlow: true,
@@ -184,42 +189,42 @@ function addNode(type: number) {
};
emits('update:childNode', data);
}
if (type === NodeType.DELAY_TIMER_NODE) {
if (type === BpmNodeTypeEnum.DELAY_TIMER_NODE) {
const data: SimpleFlowNode = {
id: `Activity_${generateUUID()}`,
name: NODE_DEFAULT_NAME.get(NodeType.DELAY_TIMER_NODE) as string,
name: NODE_DEFAULT_NAME.get(BpmNodeTypeEnum.DELAY_TIMER_NODE) as string,
showText: '',
type: NodeType.DELAY_TIMER_NODE,
type: BpmNodeTypeEnum.DELAY_TIMER_NODE,
childNode: props.childNode,
};
emits('update:childNode', data);
}
if (type === NodeType.ROUTER_BRANCH_NODE) {
if (type === BpmNodeTypeEnum.ROUTER_BRANCH_NODE) {
const data: SimpleFlowNode = {
id: `GateWay_${generateUUID()}`,
name: NODE_DEFAULT_NAME.get(NodeType.ROUTER_BRANCH_NODE) as string,
name: NODE_DEFAULT_NAME.get(BpmNodeTypeEnum.ROUTER_BRANCH_NODE) as string,
showText: '',
type: NodeType.ROUTER_BRANCH_NODE,
type: BpmNodeTypeEnum.ROUTER_BRANCH_NODE,
childNode: props.childNode,
};
emits('update:childNode', data);
}
if (type === NodeType.TRIGGER_NODE) {
if (type === BpmNodeTypeEnum.TRIGGER_NODE) {
const data: SimpleFlowNode = {
id: `Activity_${generateUUID()}`,
name: NODE_DEFAULT_NAME.get(NodeType.TRIGGER_NODE) as string,
name: NODE_DEFAULT_NAME.get(BpmNodeTypeEnum.TRIGGER_NODE) as string,
showText: '',
type: NodeType.TRIGGER_NODE,
type: BpmNodeTypeEnum.TRIGGER_NODE,
childNode: props.childNode,
};
emits('update:childNode', data);
}
if (type === NodeType.CHILD_PROCESS_NODE) {
if (type === BpmNodeTypeEnum.CHILD_PROCESS_NODE) {
const data: SimpleFlowNode = {
id: `Activity_${generateUUID()}`,
name: NODE_DEFAULT_NAME.get(NodeType.CHILD_PROCESS_NODE) as string,
name: NODE_DEFAULT_NAME.get(BpmNodeTypeEnum.CHILD_PROCESS_NODE) as string,
showText: '',
type: NodeType.CHILD_PROCESS_NODE,
type: BpmNodeTypeEnum.CHILD_PROCESS_NODE,
childNode: props.childNode,
childProcessSetting: {
calledProcessDefinitionKey: '',
@@ -247,7 +252,10 @@ function addNode(type: number) {
<Popover trigger="hover" placement="right" width="auto" v-if="!readonly">
<template #content>
<div class="handler-item-wrapper">
<div class="handler-item" @click="addNode(NodeType.USER_TASK_NODE)">
<div
class="handler-item"
@click="addNode(BpmNodeTypeEnum.USER_TASK_NODE)"
>
<div class="approve handler-item-icon">
<span class="iconfont icon-approve icon-size"></span>
</div>
@@ -255,14 +263,17 @@ function addNode(type: number) {
</div>
<div
class="handler-item"
@click="addNode(NodeType.TRANSACTOR_NODE)"
@click="addNode(BpmNodeTypeEnum.TRANSACTOR_NODE)"
>
<div class="transactor handler-item-icon">
<span class="iconfont icon-transactor icon-size"></span>
</div>
<div class="handler-item-text">办理人</div>
</div>
<div class="handler-item" @click="addNode(NodeType.COPY_TASK_NODE)">
<div
class="handler-item"
@click="addNode(BpmNodeTypeEnum.COPY_TASK_NODE)"
>
<div class="handler-item-icon copy">
<span class="iconfont icon-size icon-copy"></span>
</div>
@@ -270,7 +281,7 @@ function addNode(type: number) {
</div>
<div
class="handler-item"
@click="addNode(NodeType.CONDITION_BRANCH_NODE)"
@click="addNode(BpmNodeTypeEnum.CONDITION_BRANCH_NODE)"
>
<div class="handler-item-icon condition">
<span class="iconfont icon-size icon-exclusive"></span>
@@ -279,7 +290,7 @@ function addNode(type: number) {
</div>
<div
class="handler-item"
@click="addNode(NodeType.PARALLEL_BRANCH_NODE)"
@click="addNode(BpmNodeTypeEnum.PARALLEL_BRANCH_NODE)"
>
<div class="handler-item-icon parallel">
<span class="iconfont icon-size icon-parallel"></span>
@@ -288,7 +299,7 @@ function addNode(type: number) {
</div>
<div
class="handler-item"
@click="addNode(NodeType.INCLUSIVE_BRANCH_NODE)"
@click="addNode(BpmNodeTypeEnum.INCLUSIVE_BRANCH_NODE)"
>
<div class="handler-item-icon inclusive">
<span class="iconfont icon-size icon-inclusive"></span>
@@ -297,7 +308,7 @@ function addNode(type: number) {
</div>
<div
class="handler-item"
@click="addNode(NodeType.DELAY_TIMER_NODE)"
@click="addNode(BpmNodeTypeEnum.DELAY_TIMER_NODE)"
>
<div class="handler-item-icon delay">
<span class="iconfont icon-size icon-delay"></span>
@@ -306,14 +317,17 @@ function addNode(type: number) {
</div>
<div
class="handler-item"
@click="addNode(NodeType.ROUTER_BRANCH_NODE)"
@click="addNode(BpmNodeTypeEnum.ROUTER_BRANCH_NODE)"
>
<div class="handler-item-icon router">
<span class="iconfont icon-size icon-router"></span>
</div>
<div class="handler-item-text">路由分支</div>
</div>
<div class="handler-item" @click="addNode(NodeType.TRIGGER_NODE)">
<div
class="handler-item"
@click="addNode(BpmNodeTypeEnum.TRIGGER_NODE)"
>
<div class="handler-item-icon trigger">
<span class="iconfont icon-size icon-trigger"></span>
</div>
@@ -321,7 +335,7 @@ function addNode(type: number) {
</div>
<div
class="handler-item"
@click="addNode(NodeType.CHILD_PROCESS_NODE)"
@click="addNode(BpmNodeTypeEnum.CHILD_PROCESS_NODE)"
>
<div class="handler-item-icon child-process">
<span class="iconfont icon-size icon-child-process"></span>
@@ -330,7 +344,7 @@ function addNode(type: number) {
</div>
</div>
</template>
<div class="add-icon"><IconifyIcon icon="ep:plus" /></div>
<div class="add-icon"><IconifyIcon icon="lucide:plus" /></div>
</Popover>
</div>
</div>

View File

@@ -1,14 +1,16 @@
<script setup lang="ts">
import type { SimpleFlowNode } from '../../consts';
import { inject, ref, watch } from 'vue';
import { inject, nextTick, ref, watch } from 'vue';
import { IconifyIcon } from '@vben/icons';
import { buildShortUUID as generateUUID } from '@vben/utils';
import { Button, Input } from 'ant-design-vue';
import { NODE_DEFAULT_TEXT, NodeType } from '../../consts';
import { BpmNodeTypeEnum } from '#/utils';
import { NODE_DEFAULT_TEXT } from '../../consts';
import { useTaskStatusClass } from '../../helpers';
import ProcessNodeTree from '../process-node-tree.vue';
import NodeHandler from './node-handler.vue';
@@ -44,10 +46,28 @@ watch(
},
);
// 条件节点名称输入框引用
const inputRefs = ref<HTMLInputElement[]>([]);
// 节点名称输入框显示状态
const showInputs = ref<boolean[]>([]);
// 失去焦点
function blurEvent(index: number) {
// 监听显示状态变化
watch(
showInputs,
(newValues) => {
// 当输入框显示时, 自动聚焦
newValues.forEach((value, index) => {
if (value) {
// 当显示状态从 false 变为 true 时, 自动聚焦
nextTick(() => {
inputRefs.value[index]?.focus();
});
}
});
},
{ deep: true },
);
// 修改节点名称
function changeNodeName(index: number) {
showInputs.value[index] = false;
const conditionNode = currentNode.value.conditionNodes?.at(
index,
@@ -70,7 +90,7 @@ function addCondition() {
id: `Flow_${generateUUID()}`,
name: `并行${len}`,
showText: '无需配置条件同时执行',
type: NodeType.CONDITION_NODE,
type: BpmNodeTypeEnum.CONDITION_NODE,
childNode: undefined,
conditionNodes: [],
};
@@ -97,7 +117,7 @@ function recursiveFindParentNode(
node: SimpleFlowNode,
nodeType: number,
) {
if (!node || node.type === NodeType.START_USER_NODE) {
if (!node || node.type === BpmNodeTypeEnum.START_USER_NODE) {
return;
}
if (node.type === nodeType) {
@@ -148,10 +168,16 @@ function recursiveFindParentNode(
<div class="branch-node-title-container">
<div v-if="showInputs[index]">
<Input
:ref="
(el) => {
inputRefs[index] = el as HTMLInputElement;
}
"
type="text"
class="input-max-width editable-title-input"
@blur="blurEvent(index)"
v-model="item.name"
@blur="changeNodeName(index)"
@press-enter="changeNodeName(index)"
v-model:value="item.name"
/>
</div>
<div v-else class="branch-title" @click="clickEvent(index)">
@@ -168,14 +194,14 @@ function recursiveFindParentNode(
{{ item.showText }}
</div>
<div class="branch-node-text" v-else>
{{ NODE_DEFAULT_TEXT.get(NodeType.CONDITION_NODE) }}
{{ NODE_DEFAULT_TEXT.get(BpmNodeTypeEnum.CONDITION_NODE) }}
</div>
</div>
<div v-if="!readonly" class="node-toolbar">
<div class="toolbar-icon">
<IconifyIcon
color="#0089ff"
icon="ep:circle-close-filled"
icon="lucide:circle-x"
@click="deleteCondition(index)"
/>
</div>

View File

@@ -7,7 +7,9 @@ import { IconifyIcon } from '@vben/icons';
import { Input } from 'ant-design-vue';
import { NODE_DEFAULT_TEXT, NodeType } from '../../consts';
import { BpmNodeTypeEnum } from '#/utils';
import { NODE_DEFAULT_TEXT } from '../../consts';
import { useNodeName2, useTaskStatusClass, useWatchNode } from '../../helpers';
import RouterNodeConfig from '../nodes-config/router-node-config.vue';
import NodeHandler from './node-handler.vue';
@@ -31,9 +33,9 @@ const readonly = inject<Boolean>('readonly');
// 监控节点的变化
const currentNode = useWatchNode(props);
// 节点名称编辑
const { showInput, blurEvent, clickTitle } = useNodeName2(
const { showInput, changeNodeName, clickTitle, inputRef } = useNodeName2(
currentNode,
NodeType.ROUTER_BRANCH_NODE,
BpmNodeTypeEnum.ROUTER_BRANCH_NODE,
);
const nodeSetting = ref();
@@ -65,11 +67,13 @@ function deleteNode() {
<span class="iconfont icon-router"></span>
</div>
<Input
ref="inputRef"
v-if="!readonly && showInput"
type="text"
class="editable-title-input"
@blur="blurEvent()"
v-model="currentNode.name"
@blur="changeNodeName()"
@press-enter="changeNodeName()"
v-model:value="currentNode.name"
:placeholder="currentNode.name"
/>
<div v-else class="node-title" @click="clickTitle">
@@ -85,15 +89,15 @@ function deleteNode() {
{{ currentNode.showText }}
</div>
<div class="node-text" v-else>
{{ NODE_DEFAULT_TEXT.get(NodeType.ROUTER_BRANCH_NODE) }}
{{ NODE_DEFAULT_TEXT.get(BpmNodeTypeEnum.ROUTER_BRANCH_NODE) }}
</div>
<IconifyIcon v-if="!readonly" icon="ep:arrow-right-bold" />
<IconifyIcon v-if="!readonly" icon="lucide:chevron-right" />
</div>
<div v-if="!readonly" class="node-toolbar">
<div class="toolbar-icon">
<IconifyIcon
color="#0089ff"
icon="ep:circle-close-filled"
icon="lucide:circle-x"
@click="deleteNode"
/>
</div>

View File

@@ -1,4 +1,5 @@
<script setup lang="ts">
// TODO @芋艿:后续是不是把业务组件,挪到每个模块里;待定;
import type { Ref } from 'vue';
import type { SimpleFlowNode } from '../../consts';
@@ -9,7 +10,9 @@ import { IconifyIcon } from '@vben/icons';
import { Input } from 'ant-design-vue';
import { NODE_DEFAULT_TEXT, NodeType } from '../../consts';
import { BpmNodeTypeEnum } from '#/utils';
import { NODE_DEFAULT_TEXT } from '../../consts';
import { useNodeName2, useTaskStatusClass, useWatchNode } from '../../helpers';
import StartUserNodeConfig from '../nodes-config/start-user-node-config.vue';
import NodeHandler from './node-handler.vue';
@@ -34,9 +37,9 @@ const tasks = inject<Ref<any[]>>('tasks', ref([]));
// 监控节点变化
const currentNode = useWatchNode(props);
// 节点名称编辑
const { showInput, blurEvent, clickTitle } = useNodeName2(
const { showInput, changeNodeName, clickTitle, inputRef } = useNodeName2(
currentNode,
NodeType.START_USER_NODE,
BpmNodeTypeEnum.START_USER_NODE,
);
const nodeSetting = ref();
@@ -78,10 +81,12 @@ function nodeClick() {
<span class="iconfont icon-start-user"></span>
</div>
<Input
ref="inputRef"
v-if="!readonly && showInput"
type="text"
class="editable-title-input"
@blur="blurEvent()"
@blur="changeNodeName()"
@press-enter="changeNodeName()"
v-model:value="currentNode.name"
:placeholder="currentNode.name"
/>
@@ -98,9 +103,9 @@ function nodeClick() {
{{ currentNode.showText }}
</div>
<div class="node-text" v-else>
{{ NODE_DEFAULT_TEXT.get(NodeType.START_USER_NODE) }}
{{ NODE_DEFAULT_TEXT.get(BpmNodeTypeEnum.START_USER_NODE) }}
</div>
<IconifyIcon icon="ep:arrow-right-bold" v-if="!readonly" />
<IconifyIcon icon="lucide:chevron-right" v-if="!readonly" />
</div>
</div>
<!-- 传递子节点给添加节点组件会在子节点前面添加节点 -->

View File

@@ -7,7 +7,9 @@ import { IconifyIcon } from '@vben/icons';
import { Input } from 'ant-design-vue';
import { NODE_DEFAULT_TEXT, NodeType } from '../../consts';
import { BpmNodeTypeEnum } from '#/utils';
import { NODE_DEFAULT_TEXT } from '../../consts';
import { useNodeName2, useTaskStatusClass, useWatchNode } from '../../helpers';
import TriggerNodeConfig from '../nodes-config/trigger-node-config.vue';
import NodeHandler from './node-handler.vue';
@@ -33,9 +35,9 @@ const readonly = inject<Boolean>('readonly');
// 监控节点的变化
const currentNode = useWatchNode(props);
// 节点名称编辑
const { showInput, blurEvent, clickTitle } = useNodeName2(
const { showInput, changeNodeName, clickTitle, inputRef } = useNodeName2(
currentNode,
NodeType.TRIGGER_NODE,
BpmNodeTypeEnum.TRIGGER_NODE,
);
const nodeSetting = ref();
@@ -67,11 +69,13 @@ function deleteNode() {
<span class="iconfont icon-trigger"></span>
</div>
<Input
ref="inputRef"
v-if="!readonly && showInput"
type="text"
class="editable-title-input"
@blur="blurEvent()"
v-model="currentNode.name"
@blur="changeNodeName()"
@press-enter="changeNodeName()"
v-model:value="currentNode.name"
:placeholder="currentNode.name"
/>
<div v-else class="node-title" @click="clickTitle">
@@ -87,15 +91,15 @@ function deleteNode() {
{{ currentNode.showText }}
</div>
<div class="node-text" v-else>
{{ NODE_DEFAULT_TEXT.get(NodeType.TRIGGER_NODE) }}
{{ NODE_DEFAULT_TEXT.get(BpmNodeTypeEnum.TRIGGER_NODE) }}
</div>
<IconifyIcon v-if="!readonly" icon="ep:arrow-right-bold" />
<IconifyIcon v-if="!readonly" icon="lucide:chevron-right" />
</div>
<div v-if="!readonly" class="node-toolbar">
<div class="toolbar-icon">
<IconifyIcon
color="#0089ff"
icon="ep:circle-close-filled"
icon="lucide:circle-x"
:size="18"
@click="deleteNode"
/>

View File

@@ -9,7 +9,9 @@ import { IconifyIcon } from '@vben/icons';
import { Input } from 'ant-design-vue';
import { NODE_DEFAULT_TEXT, NodeType } from '../../consts';
import { BpmNodeTypeEnum } from '#/utils';
import { NODE_DEFAULT_TEXT } from '../../consts';
import { useNodeName2, useTaskStatusClass, useWatchNode } from '../../helpers';
import UserTaskNodeConfig from '../nodes-config/user-task-node-config.vue';
import NodeHandler from './node-handler.vue';
@@ -24,7 +26,7 @@ const props = defineProps({
});
const emits = defineEmits<{
findParentNode: [nodeList: SimpleFlowNode[], nodeType: NodeType];
findParentNode: [nodeList: SimpleFlowNode[], nodeType: BpmNodeTypeEnum];
'update:flowNode': [node: SimpleFlowNode | undefined];
}>();
@@ -34,9 +36,9 @@ const tasks = inject<Ref<any[]>>('tasks', ref([]));
// 监控节点变化
const currentNode = useWatchNode(props);
// 节点名称编辑
const { showInput, blurEvent, clickTitle } = useNodeName2(
const { showInput, changeNodeName, clickTitle, inputRef } = useNodeName2(
currentNode,
NodeType.START_USER_NODE,
BpmNodeTypeEnum.USER_TASK_NODE,
);
const nodeSetting = ref();
@@ -60,7 +62,7 @@ function findReturnTaskNodes(
matchNodeList: SimpleFlowNode[], // 匹配的节点
) {
// 从父节点查找
emits('findParentNode', matchNodeList, NodeType.USER_TASK_NODE);
emits('findParentNode', matchNodeList, BpmNodeTypeEnum.USER_TASK_NODE);
}
// const selectTasks = ref<any[] | undefined>([]); // 选中的任务数组
@@ -77,19 +79,21 @@ function findReturnTaskNodes(
>
<div class="node-title-container">
<div
:class="`node-title-icon ${currentNode.type === NodeType.TRANSACTOR_NODE ? 'transactor-task' : 'user-task'}`"
:class="`node-title-icon ${currentNode.type === BpmNodeTypeEnum.TRANSACTOR_NODE ? 'transactor-task' : 'user-task'}`"
>
<span
:class="`iconfont ${currentNode.type === NodeType.TRANSACTOR_NODE ? 'icon-transactor' : 'icon-approve'}`"
:class="`iconfont ${currentNode.type === BpmNodeTypeEnum.TRANSACTOR_NODE ? 'icon-transactor' : 'icon-approve'}`"
>
</span>
</div>
<Input
ref="inputRef"
v-if="!readonly && showInput"
type="text"
class="editable-title-input"
@blur="blurEvent()"
v-model="currentNode.name"
@blur="changeNodeName()"
@press-enter="changeNodeName()"
v-model:value="currentNode.name"
:placeholder="currentNode.name"
/>
<div v-else class="node-title" @click="clickTitle">
@@ -107,13 +111,13 @@ function findReturnTaskNodes(
<div class="node-text" v-else>
{{ NODE_DEFAULT_TEXT.get(currentNode.type) }}
</div>
<IconifyIcon icon="ep:arrow-right-bold" v-if="!readonly" />
<IconifyIcon icon="lucide:chevron-right" v-if="!readonly" />
</div>
<div v-if="!readonly" class="node-toolbar">
<div class="toolbar-icon">
<IconifyIcon
color="#0089ff"
icon="ep:circle-close-filled"
icon="lucide:circle-x"
:size="18"
@click="deleteNode"
/>

View File

@@ -1,7 +1,8 @@
<script setup lang="ts">
import type { SimpleFlowNode } from '../consts';
import { NodeType } from '../consts';
import { BpmNodeTypeEnum } from '#/utils';
import { useWatchNode } from '../helpers';
import CopyTaskNode from './nodes/copy-task-node.vue';
import DelayTimerNode from './nodes/delay-timer-node.vue';
@@ -57,7 +58,7 @@ function recursiveFindParentNode(
if (!findNode) {
return;
}
if (findNode.type === NodeType.START_USER_NODE) {
if (findNode.type === BpmNodeTypeEnum.START_USER_NODE) {
nodeList.push(findNode);
return;
}
@@ -71,15 +72,15 @@ function recursiveFindParentNode(
<template>
<!-- 发起人节点 -->
<StartUserNode
v-if="currentNode && currentNode.type === NodeType.START_USER_NODE"
v-if="currentNode && currentNode.type === BpmNodeTypeEnum.START_USER_NODE"
:flow-node="currentNode"
/>
<!-- 审批节点 -->
<UserTaskNode
v-if="
currentNode &&
(currentNode.type === NodeType.USER_TASK_NODE ||
currentNode.type === NodeType.TRANSACTOR_NODE)
(currentNode.type === BpmNodeTypeEnum.USER_TASK_NODE ||
currentNode.type === BpmNodeTypeEnum.TRANSACTOR_NODE)
"
:flow-node="currentNode"
@update:flow-node="handleModelValueUpdate"
@@ -87,46 +88,54 @@ function recursiveFindParentNode(
/>
<!-- 抄送节点 -->
<CopyTaskNode
v-if="currentNode && currentNode.type === NodeType.COPY_TASK_NODE"
v-if="currentNode && currentNode.type === BpmNodeTypeEnum.COPY_TASK_NODE"
:flow-node="currentNode"
@update:flow-node="handleModelValueUpdate"
/>
<!-- 条件节点 -->
<ExclusiveNode
v-if="currentNode && currentNode.type === NodeType.CONDITION_BRANCH_NODE"
v-if="
currentNode && currentNode.type === BpmNodeTypeEnum.CONDITION_BRANCH_NODE
"
:flow-node="currentNode"
@update:model-value="handleModelValueUpdate"
@find-parent-node="findParentNode"
/>
<!-- 并行节点 -->
<ParallelNode
v-if="currentNode && currentNode.type === NodeType.PARALLEL_BRANCH_NODE"
v-if="
currentNode && currentNode.type === BpmNodeTypeEnum.PARALLEL_BRANCH_NODE
"
:flow-node="currentNode"
@update:model-value="handleModelValueUpdate"
@find-parent-node="findParentNode"
/>
<!-- 包容分支节点 -->
<InclusiveNode
v-if="currentNode && currentNode.type === NodeType.INCLUSIVE_BRANCH_NODE"
v-if="
currentNode && currentNode.type === BpmNodeTypeEnum.INCLUSIVE_BRANCH_NODE
"
:flow-node="currentNode"
@update:model-value="handleModelValueUpdate"
@find-parent-node="findParentNode"
/>
<!-- 延迟器节点 -->
<DelayTimerNode
v-if="currentNode && currentNode.type === NodeType.DELAY_TIMER_NODE"
v-if="currentNode && currentNode.type === BpmNodeTypeEnum.DELAY_TIMER_NODE"
:flow-node="currentNode"
@update:flow-node="handleModelValueUpdate"
/>
<!-- 路由分支节点 -->
<RouterNode
v-if="currentNode && currentNode.type === NodeType.ROUTER_BRANCH_NODE"
v-if="
currentNode && currentNode.type === BpmNodeTypeEnum.ROUTER_BRANCH_NODE
"
:flow-node="currentNode"
@update:flow-node="handleModelValueUpdate"
/>
<!-- 触发器节点 -->
<TriggerNode
v-if="currentNode && currentNode.type === NodeType.TRIGGER_NODE"
v-if="currentNode && currentNode.type === BpmNodeTypeEnum.TRIGGER_NODE"
:flow-node="currentNode"
@update:flow-node="handleModelValueUpdate"
/>
@@ -146,7 +155,7 @@ function recursiveFindParentNode(
<!-- 结束节点 -->
<EndEventNode
v-if="currentNode && currentNode.type === NodeType.END_EVENT_NODE"
v-if="currentNode && currentNode.type === BpmNodeTypeEnum.END_EVENT_NODE"
:flow-node="currentNode"
/>
</template>

View File

@@ -22,9 +22,9 @@ import { getSimpleDeptList } from '#/api/system/dept';
import { getSimplePostList } from '#/api/system/post';
import { getSimpleRoleList } from '#/api/system/role';
import { getSimpleUserList } from '#/api/system/user';
import { BpmModelFormType } from '#/utils/constants';
import { BpmModelFormType, BpmNodeTypeEnum } from '#/utils';
import { NODE_DEFAULT_TEXT, NodeId, NodeType } from '../consts';
import { NODE_DEFAULT_TEXT, NodeId } from '../consts';
import SimpleProcessModel from './simple-process-model.vue';
defineOptions({
@@ -97,7 +97,7 @@ const postOptions = ref<SystemPostApi.Post[]>([]); // 岗位列表
const userOptions = ref<SystemUserApi.User[]>([]); // 用户列表
const deptOptions = ref<SystemDeptApi.Dept[]>([]); // 部门列表
const deptTreeOptions = ref();
const userGroupOptions = ref<BpmUserGroupApi.UserGroupVO[]>([]); // 用户组列表
const userGroupOptions = ref<BpmUserGroupApi.UserGroup[]>([]); // 用户组列表
provide('formFields', formFields);
provide('formType', formType);
@@ -124,13 +124,13 @@ function updateModel() {
if (!processNodeTree.value) {
processNodeTree.value = {
name: '发起人',
type: NodeType.START_USER_NODE,
type: BpmNodeTypeEnum.START_USER_NODE,
id: NodeId.START_USER_NODE_ID,
showText: '默认配置',
childNode: {
id: NodeId.END_EVENT_NODE_ID,
name: '结束',
type: NodeType.END_EVENT_NODE,
type: BpmNodeTypeEnum.END_EVENT_NODE,
},
};
// 初始化时也触发一次保存
@@ -162,14 +162,14 @@ function validateNode(
) {
if (node) {
const { type, showText, conditionNodes } = node;
if (type === NodeType.END_EVENT_NODE) {
if (type === BpmNodeTypeEnum.END_EVENT_NODE) {
return;
}
if (
type === NodeType.CONDITION_BRANCH_NODE ||
type === NodeType.PARALLEL_BRANCH_NODE ||
type === NodeType.INCLUSIVE_BRANCH_NODE
type === BpmNodeTypeEnum.CONDITION_BRANCH_NODE ||
type === BpmNodeTypeEnum.PARALLEL_BRANCH_NODE ||
type === BpmNodeTypeEnum.INCLUSIVE_BRANCH_NODE
) {
// 1. 分支节点, 先校验各个分支
conditionNodes?.forEach((item) => {
@@ -235,7 +235,7 @@ defineExpose({ validate });
:readonly="false"
@save="saveSimpleFlowModel"
/>
<ErrorModal title="流程设计校验不通过" class="w-[600px]">
<ErrorModal title="流程设计校验不通过" class="w-[40%]">
<div class="mb-2 text-base">以下节点配置不完善请修改相关配置</div>
<div
class="mb-3 rounded-md bg-gray-100 p-2 text-sm"

View File

@@ -8,7 +8,9 @@ import { downloadFileFromBlob, isString } from '@vben/utils';
import { Button, ButtonGroup, Modal, Row } from 'ant-design-vue';
import { NODE_DEFAULT_TEXT, NodeType } from '../consts';
import { BpmNodeTypeEnum } from '#/utils';
import { NODE_DEFAULT_TEXT } from '../consts';
import { useWatchNode } from '../helpers';
import ProcessNodeTree from './process-node-tree.vue';
@@ -113,18 +115,18 @@ function validateNode(
) {
if (node) {
const { type, showText, conditionNodes } = node;
if (type === NodeType.END_EVENT_NODE) {
if (type === BpmNodeTypeEnum.END_EVENT_NODE) {
return;
}
if (type === NodeType.START_USER_NODE) {
if (type === BpmNodeTypeEnum.START_USER_NODE) {
// 发起人节点暂时不用校验,直接校验孩子节点
validateNode(node.childNode, errorNodes);
}
if (
type === NodeType.USER_TASK_NODE ||
type === NodeType.COPY_TASK_NODE ||
type === NodeType.CONDITION_NODE
type === BpmNodeTypeEnum.USER_TASK_NODE ||
type === BpmNodeTypeEnum.COPY_TASK_NODE ||
type === BpmNodeTypeEnum.CONDITION_NODE
) {
if (!showText) {
errorNodes.push(node);
@@ -133,9 +135,9 @@ function validateNode(
}
if (
type === NodeType.CONDITION_BRANCH_NODE ||
type === NodeType.PARALLEL_BRANCH_NODE ||
type === NodeType.INCLUSIVE_BRANCH_NODE
type === BpmNodeTypeEnum.CONDITION_BRANCH_NODE ||
type === BpmNodeTypeEnum.PARALLEL_BRANCH_NODE ||
type === BpmNodeTypeEnum.INCLUSIVE_BRANCH_NODE
) {
// 分支节点
// 1. 先校验各个分支
@@ -203,10 +205,10 @@ onMounted(() => {
<Row type="flex" justify="end">
<ButtonGroup key="scale-control">
<Button v-if="!readonly" @click="exportJson">
<IconifyIcon icon="ep:download" /> 导出
<IconifyIcon icon="lucide:download" /> 导出
</Button>
<Button v-if="!readonly" @click="importJson">
<IconifyIcon icon="ep:upload" />导入
<IconifyIcon icon="lucide:upload" />导入
</Button>
<!-- 用于打开本地文件-->
<input
@@ -219,14 +221,14 @@ onMounted(() => {
@change="importLocalFile"
/>
<Button @click="processReZoom()">
<IconifyIcon icon="tabler:relation-one-to-one" />
<IconifyIcon icon="lucide:table-columns-split" />
</Button>
<Button :plain="true" @click="zoomOut()">
<IconifyIcon icon="tabler:zoom-out" />
<IconifyIcon icon="lucide:zoom-out" />
</Button>
<Button class="w-80px"> {{ scaleValue }}% </Button>
<Button :plain="true" @click="zoomIn()">
<IconifyIcon icon="tabler:zoom-in" />
<IconifyIcon icon="lucide:zoom-in" />
</Button>
<Button @click="resetPosition">重置</Button>
</ButtonGroup>

View File

@@ -1,4 +1,4 @@
// TODO 芋艿 这些 常量是不是可以共享
import { BpmNodeTypeEnum, BpmTaskStatusEnum } from '#/utils';
interface DictDataType {
label: string;
@@ -43,112 +43,6 @@ export enum ApproveMethodType {
SEQUENTIAL_APPROVE = 4,
}
/**
* 任务状态枚举
*/
export enum TaskStatusEnum {
/**
* 审批通过
*/
APPROVE = 2,
/**
* 审批通过中
*/
APPROVING = 7,
/**
* 已取消
*/
CANCEL = 4,
/**
* 未开始
*/
NOT_START = -1,
/**
* 审批不通过
*/
REJECT = 3,
/**
* 已退回
*/
RETURN = 5,
/**
* 审批中
*/
RUNNING = 1,
/**
* 待审批
*/
WAIT = 0,
}
/**
* 节点类型
*/
export enum NodeType {
/**
* 子流程节点
*/
CHILD_PROCESS_NODE = 20,
/**
* 条件分支节点 (对应排他网关)
*/
CONDITION_BRANCH_NODE = 51,
/**
* 条件节点
*/
CONDITION_NODE = 50,
/**
* 抄送人节点
*/
COPY_TASK_NODE = 12,
/**
* 延迟器节点
*/
DELAY_TIMER_NODE = 14,
/**
* 结束节点
*/
END_EVENT_NODE = 1,
/**
* 包容分支节点 (对应包容网关)
*/
INCLUSIVE_BRANCH_NODE = 53,
/**
* 并行分支节点 (对应并行网关)
*/
PARALLEL_BRANCH_NODE = 52,
/**
* 路由分支节点
*/
ROUTER_BRANCH_NODE = 54,
/**
* 发起人节点
*/
START_USER_NODE = 10,
/**
* 办理人节点
*/
TRANSACTOR_NODE = 13,
/**
* 触发器节点
*/
TRIGGER_NODE = 15,
/**
* 审批人节点
*/
USER_TASK_NODE = 11,
}
export enum NodeId {
/**
* 发起人节点 Id
@@ -660,7 +554,7 @@ export type ChildProcessSetting = {
*/
export interface SimpleFlowNode {
id: string;
type: NodeType;
type: BpmNodeTypeEnum;
name: string;
showText?: string;
// 孩子节点
@@ -698,7 +592,7 @@ export interface SimpleFlowNode {
// 条件设置
conditionSetting?: ConditionSetting;
// 活动的状态,用于前端节点状态展示
activityStatus?: TaskStatusEnum;
activityStatus?: BpmTaskStatusEnum;
// 延迟设置
delaySetting?: DelaySetting;
// 路由分支
@@ -734,26 +628,26 @@ export const DEFAULT_CONDITION_GROUP_VALUE = {
};
export const NODE_DEFAULT_TEXT = new Map<number, string>();
NODE_DEFAULT_TEXT.set(NodeType.USER_TASK_NODE, '请配置审批人');
NODE_DEFAULT_TEXT.set(NodeType.COPY_TASK_NODE, '请配置抄送人');
NODE_DEFAULT_TEXT.set(NodeType.CONDITION_NODE, '请设置条件');
NODE_DEFAULT_TEXT.set(NodeType.START_USER_NODE, '请设置发起人');
NODE_DEFAULT_TEXT.set(NodeType.DELAY_TIMER_NODE, '请设置延迟器');
NODE_DEFAULT_TEXT.set(NodeType.ROUTER_BRANCH_NODE, '请设置路由节点');
NODE_DEFAULT_TEXT.set(NodeType.TRIGGER_NODE, '请设置触发器');
NODE_DEFAULT_TEXT.set(NodeType.TRANSACTOR_NODE, '请设置办理人');
NODE_DEFAULT_TEXT.set(NodeType.CHILD_PROCESS_NODE, '请设置子流程');
NODE_DEFAULT_TEXT.set(BpmNodeTypeEnum.USER_TASK_NODE, '请配置审批人');
NODE_DEFAULT_TEXT.set(BpmNodeTypeEnum.COPY_TASK_NODE, '请配置抄送人');
NODE_DEFAULT_TEXT.set(BpmNodeTypeEnum.CONDITION_NODE, '请设置条件');
NODE_DEFAULT_TEXT.set(BpmNodeTypeEnum.START_USER_NODE, '请设置发起人');
NODE_DEFAULT_TEXT.set(BpmNodeTypeEnum.DELAY_TIMER_NODE, '请设置延迟器');
NODE_DEFAULT_TEXT.set(BpmNodeTypeEnum.ROUTER_BRANCH_NODE, '请设置路由节点');
NODE_DEFAULT_TEXT.set(BpmNodeTypeEnum.TRIGGER_NODE, '请设置触发器');
NODE_DEFAULT_TEXT.set(BpmNodeTypeEnum.TRANSACTOR_NODE, '请设置办理人');
NODE_DEFAULT_TEXT.set(BpmNodeTypeEnum.CHILD_PROCESS_NODE, '请设置子流程');
export const NODE_DEFAULT_NAME = new Map<number, string>();
NODE_DEFAULT_NAME.set(NodeType.USER_TASK_NODE, '审批人');
NODE_DEFAULT_NAME.set(NodeType.COPY_TASK_NODE, '抄送人');
NODE_DEFAULT_NAME.set(NodeType.CONDITION_NODE, '条件');
NODE_DEFAULT_NAME.set(NodeType.START_USER_NODE, '发起人');
NODE_DEFAULT_NAME.set(NodeType.DELAY_TIMER_NODE, '延迟器');
NODE_DEFAULT_NAME.set(NodeType.ROUTER_BRANCH_NODE, '路由分支');
NODE_DEFAULT_NAME.set(NodeType.TRIGGER_NODE, '触发器');
NODE_DEFAULT_NAME.set(NodeType.TRANSACTOR_NODE, '办理人');
NODE_DEFAULT_NAME.set(NodeType.CHILD_PROCESS_NODE, '子流程');
NODE_DEFAULT_NAME.set(BpmNodeTypeEnum.USER_TASK_NODE, '审批人');
NODE_DEFAULT_NAME.set(BpmNodeTypeEnum.COPY_TASK_NODE, '抄送人');
NODE_DEFAULT_NAME.set(BpmNodeTypeEnum.CONDITION_NODE, '条件');
NODE_DEFAULT_NAME.set(BpmNodeTypeEnum.START_USER_NODE, '发起人');
NODE_DEFAULT_NAME.set(BpmNodeTypeEnum.DELAY_TIMER_NODE, '延迟器');
NODE_DEFAULT_NAME.set(BpmNodeTypeEnum.ROUTER_BRANCH_NODE, '路由分支');
NODE_DEFAULT_NAME.set(BpmNodeTypeEnum.TRIGGER_NODE, '触发器');
NODE_DEFAULT_NAME.set(BpmNodeTypeEnum.TRANSACTOR_NODE, '办理人');
NODE_DEFAULT_NAME.set(BpmNodeTypeEnum.CHILD_PROCESS_NODE, '子流程');
// 候选人策略。暂时不从字典中取。 后续可能调整。控制显示顺序
export const CANDIDATE_STRATEGY: DictDataType[] = [
@@ -930,24 +824,6 @@ export const MULTI_LEVEL_DEPT: DictDataType[] = [
{ label: '第 15 级部门', value: 15 },
];
/**
* 流程实例的变量枚举
*/
export enum ProcessVariableEnum {
/**
* 流程定义名称
*/
PROCESS_DEFINITION_NAME = 'PROCESS_DEFINITION_NAME',
/**
* 发起时间
*/
START_TIME = 'PROCESS_START_TIME',
/**
* 发起用户 ID
*/
START_USER_ID = 'PROCESS_START_USER_ID',
}
export const DELAY_TYPE = [
{ label: '固定时长', value: DelayTypeEnum.FIXED_TIME_DURATION },
{ label: '固定日期', value: DelayTypeEnum.FIXED_DATE_TIME },

View File

@@ -12,7 +12,13 @@ import type { SystemPostApi } from '#/api/system/post';
import type { SystemRoleApi } from '#/api/system/role';
import type { SystemUserApi } from '#/api/system/user';
import { inject, ref, toRaw, unref, watch } from 'vue';
import { inject, nextTick, ref, toRaw, unref, watch } from 'vue';
import {
BpmNodeTypeEnum,
BpmTaskStatusEnum,
ProcessVariableEnum,
} from '#/utils';
import {
ApproveMethodType,
@@ -23,10 +29,7 @@ import {
ConditionType,
FieldPermissionType,
NODE_DEFAULT_NAME,
NodeType,
ProcessVariableEnum,
RejectHandlerType,
TaskStatusEnum,
} from './consts';
export function useWatchNode(props: {
@@ -252,12 +255,12 @@ export type CopyTaskFormType = {
/**
* @description 节点表单数据。 用于审批节点、抄送节点
*/
export function useNodeForm(nodeType: NodeType) {
export function useNodeForm(nodeType: BpmNodeTypeEnum) {
const roleOptions = inject<Ref<SystemRoleApi.Role[]>>('roleList', ref([])); // 角色列表
const postOptions = inject<Ref<SystemPostApi.Post[]>>('postList', ref([])); // 岗位列表
const userOptions = inject<Ref<SystemUserApi.User[]>>('userList', ref([])); // 用户列表
const deptOptions = inject<Ref<SystemDeptApi.Dept[]>>('deptList', ref([])); // 部门列表
const userGroupOptions = inject<Ref<BpmUserGroupApi.UserGroupVO[]>>(
const userGroupOptions = inject<Ref<BpmUserGroupApi.UserGroup[]>>(
'userGroupList',
ref([]),
); // 用户组列表
@@ -269,8 +272,8 @@ export function useNodeForm(nodeType: NodeType) {
const configForm = ref<any | CopyTaskFormType | UserTaskFormType>();
if (
nodeType === NodeType.USER_TASK_NODE ||
nodeType === NodeType.TRANSACTOR_NODE
nodeType === BpmNodeTypeEnum.USER_TASK_NODE ||
nodeType === BpmNodeTypeEnum.TRANSACTOR_NODE
) {
configForm.value = {
candidateStrategy: CandidateStrategy.USER,
@@ -614,37 +617,65 @@ export function useDrawer() {
/**
* @description 节点名称配置
*/
export function useNodeName(nodeType: NodeType) {
export function useNodeName(nodeType: BpmNodeTypeEnum) {
// 节点名称
const nodeName = ref<string>();
// 节点名称输入框
const showInput = ref(false);
// 输入框的引用
const inputRef = ref<HTMLInputElement | null>(null);
// 点击节点名称编辑图标
function clickIcon() {
showInput.value = true;
}
// 节点名称输入框失去焦点
function blurEvent() {
// 修改节点名称
function changeNodeName() {
showInput.value = false;
nodeName.value =
nodeName.value || (NODE_DEFAULT_NAME.get(nodeType) as string);
}
// 监听 showInput 的变化,当变为 true 时自动聚焦
watch(showInput, (value) => {
if (value) {
nextTick(() => {
inputRef.value?.focus();
});
}
});
return {
nodeName,
showInput,
inputRef,
clickIcon,
blurEvent,
changeNodeName,
};
}
export function useNodeName2(node: Ref<SimpleFlowNode>, nodeType: NodeType) {
export function useNodeName2(
node: Ref<SimpleFlowNode>,
nodeType: BpmNodeTypeEnum,
) {
// 显示节点名称输入框
const showInput = ref(false);
// 节点名称输入框失去焦点
function blurEvent() {
// 输入框的引用
const inputRef = ref<HTMLInputElement | null>(null);
// 监听 showInput 的变化,当变为 true 时自动聚焦
watch(showInput, (value) => {
if (value) {
nextTick(() => {
inputRef.value?.focus();
});
}
});
// 修改节点名称
function changeNodeName() {
showInput.value = false;
node.value.name =
node.value.name || (NODE_DEFAULT_NAME.get(nodeType) as string);
console.warn('node.value.name===>', node.value.name);
}
// 点击节点标题进行输入
function clickTitle() {
@@ -652,8 +683,9 @@ export function useNodeName2(node: Ref<SimpleFlowNode>, nodeType: NodeType) {
}
return {
showInput,
inputRef,
clickTitle,
blurEvent,
changeNodeName,
};
}
@@ -661,21 +693,21 @@ export function useNodeName2(node: Ref<SimpleFlowNode>, nodeType: NodeType) {
* @description 根据节点任务状态,获取节点任务状态样式
*/
export function useTaskStatusClass(
taskStatus: TaskStatusEnum | undefined,
taskStatus: BpmTaskStatusEnum | undefined,
): string {
if (!taskStatus) {
return '';
}
if (taskStatus === TaskStatusEnum.APPROVE) {
if (taskStatus === BpmTaskStatusEnum.APPROVE) {
return 'status-pass';
}
if (taskStatus === TaskStatusEnum.RUNNING) {
if (taskStatus === BpmTaskStatusEnum.RUNNING) {
return 'status-running';
}
if (taskStatus === TaskStatusEnum.REJECT) {
if (taskStatus === BpmTaskStatusEnum.REJECT) {
return 'status-reject';
}
if (taskStatus === TaskStatusEnum.CANCEL) {
if (taskStatus === BpmTaskStatusEnum.CANCEL) {
return 'status-cancel';
}
return '';

View File

@@ -1,3 +1,7 @@
import './styles/simple-process-designer.scss';
export { default as HttpRequestSetting } from './components/nodes-config/modules/http-request-setting.vue';
export { default as SimpleProcessDesigner } from './components/simple-process-designer.vue';
export { parseFormFields } from './helpers';

View File

@@ -0,0 +1,2 @@
export { default as SummaryCard } from './summary-card.vue';
export type { SummaryCardProps } from './typing';

View File

@@ -0,0 +1,57 @@
<script lang="ts" setup>
import type { SummaryCardProps } from './typing';
import { CountTo } from '@vben/common-ui';
import { IconifyIcon } from '@vben/icons';
import { Tooltip } from 'ant-design-vue';
/** 统计卡片 */
defineOptions({ name: 'SummaryCard' });
defineProps<SummaryCardProps>();
</script>
<template>
<div
class="flex flex-row items-center gap-3 rounded bg-[var(--el-bg-color-overlay)] p-4"
>
<div
class="rounded-1 flex h-12 w-12 flex-shrink-0 items-center justify-center"
:class="`${iconColor} ${iconBgColor}`"
>
<IconifyIcon v-if="icon" :icon="icon" class="!text-6" />
</div>
<div class="flex flex-col gap-1">
<div class="flex items-center gap-1">
<span class="text-3.5">{{ title }}</span>
<Tooltip :content="tooltip" placement="topLeft" v-if="tooltip">
<IconifyIcon
icon="lucide:circle-alert"
class="item-center !text-3 flex"
/>
</Tooltip>
</div>
<div class="flex flex-row items-baseline gap-2">
<div class="text-7">
<CountTo
:prefix="prefix"
:end-val="value ?? 0"
:decimals="decimals ?? 0"
/>
</div>
<span
v-if="percent !== undefined"
:class="Number(percent) > 0 ? 'text-red-500' : 'text-green-500'"
>
<span class="text-sm">{{ Math.abs(Number(percent)) }}%</span>
<IconifyIcon
:icon="
Number(percent) > 0 ? 'lucide:chevron-up' : 'lucide:chevron-down'
"
class="!text-3 ml-0.5"
/>
</span>
</div>
</div>
</div>
</template>

View File

@@ -0,0 +1,11 @@
export interface SummaryCardProps {
title: string;
tooltip?: string;
icon?: string;
iconColor?: string;
iconBgColor?: string;
prefix?: string;
value?: number;
decimals?: number;
percent?: number | string;
}

View File

@@ -43,28 +43,27 @@ const { hasAccessByCodes } = useAccess();
function isIfShow(action: ActionItem): boolean {
const ifShow = action.ifShow;
let isIfShow = true;
if (isBoolean(ifShow)) {
isIfShow = ifShow;
}
if (isFunction(ifShow)) {
isIfShow = ifShow(action);
}
if (isIfShow) {
isIfShow =
hasAccessByCodes(action.auth || []) || (action.auth || []).length === 0;
}
return isIfShow;
}
const getActions = computed(() => {
return (toRaw(props.actions) || [])
.filter((action) => {
return (
(hasAccessByCodes(action.auth || []) ||
(action.auth || []).length === 0) &&
isIfShow(action)
);
const actions = toRaw(props.actions) || [];
return actions
.filter((action: ActionItem) => {
return isIfShow(action);
})
.map((action) => {
.map((action: ActionItem) => {
const { popConfirm } = action;
return {
type: action.type || 'link',
@@ -78,24 +77,21 @@ const getActions = computed(() => {
});
const getDropdownList = computed((): any[] => {
return (toRaw(props.dropDownActions) || [])
.filter((action) => {
return (
(hasAccessByCodes(action.auth || []) ||
(action.auth || []).length === 0) &&
isIfShow(action)
);
const dropDownActions = toRaw(props.dropDownActions) || [];
return dropDownActions
.filter((action: ActionItem) => {
return isIfShow(action);
})
.map((action, index) => {
.map((action: ActionItem, index: number) => {
const { label, popConfirm } = action;
delete action.icon;
return {
...action,
...popConfirm,
onConfirm: popConfirm?.confirm,
onCancel: popConfirm?.cancel,
text: label,
divider:
index < props.dropDownActions.length - 1 ? props.divider : false,
divider: index < dropDownActions.length - 1 ? props.divider : false,
};
});
});

View File

@@ -100,7 +100,6 @@ async function handleRemove(file: UploadFile) {
}
async function beforeUpload(file: File) {
// 使用现代的Blob.text()方法替代FileReader
const fileContent = await file.text();
emit('returnText', fileContent);

View File

@@ -1,4 +1,5 @@
<script setup lang="ts">
// TODO @xingyu这个组件只有 pay 在用,和现有的 file-upload 和 image-upload 有点不一致。是不是可以考虑移除,只在 pay 那搞个复用的组件;
import type { InputProps, TextAreaProps } from 'ant-design-vue';
import type { FileUploadProps } from './typing';

View File

@@ -136,7 +136,7 @@ export function getUploadUrl(): string {
* @param file 文件
*/
function createFile0(
vo: InfraFileApi.FilePresignedUrlRespVO,
vo: InfraFileApi.FilePresignedUrlResp,
file: File,
): InfraFileApi.File {
const fileVO = {