Pre Merge pull request !105 from Jason/dev

pull/105/MERGE
Jason 2025-05-16 06:47:36 +00:00 committed by Gitee
commit 279cff5232
No known key found for this signature in database
GPG Key ID: 173E9B9CA92EEF8F
5 changed files with 945 additions and 5 deletions

View File

@ -9,6 +9,8 @@ export namespace BpmProcessDefinitionApi {
version: number;
deploymentTime: number;
suspensionState: number;
modelType: number;
modelId: string;
formType?: number;
bpmnXml?: string;
simpleModel?: string;

View File

@ -62,6 +62,18 @@ const routes: RouteRecordRaw[] = [
};
},
},
{
path: 'manager/model/create',
component: () => import('#/views/bpm/model/form/index.vue'),
name: 'BpmModelCreate',
meta: {
title: '创建流程',
activePath: '/bpm/manager/model',
icon: 'carbon:flow-connection',
hideInMenu: true,
keepAlive: true,
},
},
],
},
];

View File

@ -0,0 +1,493 @@
<script setup lang="ts">
import type { BpmCategoryApi } from '#/api/bpm/category';
import type { BpmProcessDefinitionApi } from '#/api/bpm/definition';
import type { SystemDeptApi } from '#/api/system/dept';
import type { SystemUserApi } from '#/api/system/user';
import { onBeforeUnmount, onMounted, provide, ref, watch } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { confirm, Page } from '@vben/common-ui';
import { useTabs } from '@vben/hooks';
import { ArrowLeft } from '@vben/icons';
import { useUserStore } from '@vben/stores';
import { Button, message } from 'ant-design-vue';
import { getCategorySimpleList } from '#/api/bpm/category';
import { getProcessDefinition } from '#/api/bpm/definition';
import {
createModel,
deployModel,
getModel,
updateModel,
} from '#/api/bpm/model';
import { getSimpleDeptList } from '#/api/system/dept';
import { getSimpleUserList } from '#/api/system/user';
import BasicInfo from './modules/basic-info.vue';
defineOptions({ name: 'BpmModelCreate' });
// TODO apps 使 @utils/constant.ts @
const BpmModelType = {
BPMN: 10, // BPMN
SIMPLE: 20, //
};
const BpmModelFormType = {
NORMAL: 10, //
CUSTOM: 20, //
};
const BpmAutoApproveType = {
NONE: 0, //
APPROVE_ALL: 1, //
APPROVE_SEQUENT: 2, //
};
//
type BpmProcessDefinitionType = Omit<
BpmProcessDefinitionApi.ProcessDefinitionVO,
'modelId' | 'modelType'
> & {
id?: string;
type?: number;
};
const router = useRouter();
const route = useRoute();
const userStore = useUserStore();
//
const basicInfoRef = ref();
/** 步骤校验函数 */
const validateBasic = async () => {
await basicInfoRef.value?.validate();
};
/** 表单设计校验 */
const validateForm = async () => {
// TODO
};
/** 流程设计校验 */
const validateProcess = async () => {
// TODO
};
const currentStep = ref(-1); // -1
const steps = [
{ title: '基本信息', validator: validateBasic },
{ title: '表单设计', validator: validateForm },
{ title: '流程设计', validator: validateProcess },
{ title: '更多设置', validator: null },
];
//
const formData: any = ref({
id: undefined,
name: '',
key: '',
category: undefined,
icon: undefined,
description: '',
type: BpmModelType.BPMN,
formType: BpmModelFormType.NORMAL,
formId: '',
formCustomCreatePath: '',
formCustomViewPath: '',
visible: true,
startUserType: undefined,
startUserIds: [],
startDeptIds: [],
managerUserIds: [],
allowCancelRunningProcess: true,
processIdRule: {
enable: false,
prefix: '',
infix: '',
postfix: '',
length: 5,
},
autoApprovalType: BpmAutoApproveType.NONE,
titleSetting: {
enable: false,
title: '',
},
summarySetting: {
enable: false,
summary: [],
},
});
//
const processData = ref<any>();
provide('processData', processData);
provide('modelData', formData);
//
// const formList = ref([])
const categoryList = ref<BpmCategoryApi.CategoryVO[]>([]);
const userList = ref<SystemUserApi.User[]>([]);
const deptList = ref<SystemDeptApi.Dept[]>([]);
/** 初始化数据 */
const actionType = route.params.type as string;
const initData = async () => {
if (actionType === 'definition') {
//
const definitionId = route.params.id as string;
const data = await getProcessDefinition(definitionId);
const processDefinition: BpmProcessDefinitionType = data;
// definition => model
processDefinition.type = data.modelType;
processDefinition.id = data.modelId;
if (data.simpleModel) {
processDefinition.simpleModel = JSON.parse(data.simpleModel);
}
formData.value = processDefinition;
// startUserType
if (formData.value.startUserIds?.length > 0) {
formData.value.startUserType = 1;
} else if (formData.value.startDeptIds?.length > 0) {
formData.value.startUserType = 2;
} else {
formData.value.startUserType = 0;
}
} else if (['copy', 'update'].includes(actionType)) {
// /
const modelId = route.params.id as string;
formData.value = await getModel(modelId);
// startUserType
if (formData.value.startUserIds?.length > 0) {
formData.value.startUserType = 1;
} else if (formData.value.startDeptIds?.length > 0) {
formData.value.startUserType = 2;
} else {
formData.value.startUserType = 0;
}
//
if (route.params.type === 'copy') {
delete formData.value.id;
formData.value.name += '副本';
formData.value.key += '_copy';
}
} else {
//
formData.value.startUserType = 0; //
formData.value.managerUserIds.push(userStore.userInfo?.userId);
}
// TODO
// formList.value = await getFormSimpleList()
categoryList.value = await getCategorySimpleList();
//
userList.value = await getSimpleUserList();
//
deptList.value = await getSimpleDeptList();
// currentStep
currentStep.value = 0;
// TODO
// extraSettingsRef.value.initData()
};
/** 根据类型切换流程数据 */
watch(
async () => formData.value.type,
() => {
if (formData.value.type === BpmModelType.BPMN) {
processData.value = formData.value.bpmnXml;
} else if (formData.value.type === BpmModelType.SIMPLE) {
processData.value = formData.value.simpleModel;
}
},
{
immediate: true,
},
);
/** 校验所有步骤数据是否完整 */
const validateAllSteps = async () => {
//
try {
await validateBasic();
} catch {
currentStep.value = 0;
throw new Error('请完善基本信息');
}
//
try {
await validateForm();
} catch {
currentStep.value = 1;
throw new Error('请完善自定义表单信息');
}
// TODO
try {
await validateProcess();
} catch {
currentStep.value = 2;
throw new Error('请设计流程');
}
//
try {
await validateProcess();
} catch {
currentStep.value = 2;
throw new Error('请设计流程');
}
return true;
};
/** 保存操作 */
const handleSave = async () => {
try {
//
await validateAllSteps();
//
const modelData = {
...formData.value,
};
switch (actionType) {
case 'copy': {
//
formData.value.id = await createModel(modelData);
//
message.success('复制成功,可点击【发布】按钮,进行发布模型');
break;
}
case 'definition': {
//
await updateModel(modelData);
//
message.success('恢复成功,可点击【发布】按钮,进行发布模型');
break;
}
case 'update': {
//
await updateModel(modelData);
//
message.success('修改成功,可点击【发布】按钮,进行发布模型');
break;
}
default: {
//
formData.value.id = await createModel(modelData);
//
message.success('新建成功,可点击【发布】按钮,进行发布模型');
}
}
//
if (actionType !== 'update') {
await router.push({ name: 'BpmModel' });
}
} catch (error: any) {
console.error('保存失败:', error);
message.warning(error.message || '请完善所有步骤的必填信息');
}
};
/** 发布操作 */
const handleDeploy = async () => {
try {
//
if (!formData.value.id) {
await confirm('是否确认发布该流程?');
}
//
await validateAllSteps();
//
const modelData = {
...formData.value,
};
//
if (formData.value.id) {
await updateModel(modelData);
} else {
const result = await createModel(modelData);
formData.value.id = result.id;
}
//
await deployModel(formData.value.id);
message.success('发布成功');
// TODO
await router.push({ name: 'BpmModel' });
} catch (error: any) {
console.error('发布失败:', error);
message.warning(error.message || '发布失败');
}
};
/** 步骤切换处理 */
const handleStepClick = async (index: number) => {
try {
if (index !== 0) {
await validateBasic();
}
if (index !== 1) {
await validateForm();
}
if (index !== 2) {
await validateProcess();
}
//
currentStep.value = index;
//
if (index === 2) {
// TODO
// await nextTick();
// //
// await new Promise((resolve) => setTimeout(resolve, 200));
// if (processDesignRef.value?.refresh) {
// await processDesignRef.value.refresh();
// }
}
} catch (error) {
console.error('步骤切换失败:', error);
message.warning('请先完善当前步骤必填信息');
}
};
const tabs = useTabs();
/** 返回列表页 */
const handleBack = () => {
//
tabs.closeCurrentTab();
// 使 name 'name'+ menuId
router.push({ path: '/bpm/manager/model' });
};
/** 初始化 */
onMounted(async () => {
await initData();
});
/** 添加组件卸载前的清理 */
onBeforeUnmount(() => {
//
basicInfoRef.value = null;
// TODO
// formDesignRef.value = null;
// processDesignRef.value = null;
});
</script>
<template>
<Page auto-content-height>
<div class="mx-auto">
<!-- 头部导航栏 -->
<div
class="absolute inset-x-0 top-0 z-10 flex h-12 items-center border-b bg-white px-5"
>
<!-- 左侧标题 -->
<div class="flex w-[200px] items-center overflow-hidden">
<ArrowLeft
class="size-5 flex-shrink-0 cursor-pointer"
@click="handleBack"
/>
<span
class="ml-2.5 truncate text-base"
:title="formData.name || '创建流程'"
>
{{ formData.name || '创建流程' }}
</span>
</div>
<!-- 步骤条 -->
<div class="flex h-full flex-1 items-center justify-center">
<div class="flex h-full w-[400px] items-center justify-between">
<div
v-for="(step, index) in steps"
:key="index"
class="relative mx-[15px] flex h-full cursor-pointer items-center"
:class="[
currentStep === index
? 'border-b-2 border-solid border-blue-500 text-blue-500'
: 'text-gray-500',
]"
@click="handleStepClick(index)"
>
<div
class="mr-2 flex h-7 w-7 items-center justify-center rounded-full border-2 border-solid text-[15px]"
:class="[
currentStep === index
? 'border-blue-500 bg-blue-500 text-white'
: 'border-gray-300 bg-white text-gray-500',
]"
>
{{ index + 1 }}
</div>
<span class="whitespace-nowrap text-base font-bold">{{
step.title
}}</span>
</div>
</div>
</div>
<!-- 右侧按钮 -->
<div class="flex w-[200px] items-center justify-end gap-2">
<Button
v-if="actionType === 'update'"
type="primary"
@click="handleDeploy"
>
</Button>
<Button type="primary" @click="handleSave">
<span v-if="actionType === 'definition'"> </span>
<span v-else> </span>
</Button>
</div>
</div>
<!-- 主体内容 -->
<div class="mt-[50px]">
<!-- 第一步基本信息 -->
<div v-if="currentStep === 0" class="mx-auto w-[560px]">
<BasicInfo
v-model="formData"
:category-list="categoryList"
:user-list="userList"
:dept-list="deptList"
ref="basicInfoRef"
/>
</div>
<!-- 第二步表单设计 TODO -->
<!-- 第三步流程设计 TODO -->
<!-- 第四步更多设置 TODO -->
<div v-show="currentStep === 3" class="mx-auto w-[700px]"></div>
</div>
</div>
</Page>
</template>

View File

@ -0,0 +1,432 @@
<script lang="ts" setup>
import type { Rule } from 'ant-design-vue/es/form';
import type { SelectValue } from 'ant-design-vue/es/select';
import type { BpmCategoryApi } from '#/api/bpm/category';
import type { SystemDeptApi } from '#/api/system/dept';
import type { SystemUserApi } from '#/api/system/user';
import { ref, watch } from 'vue';
import { IconifyIcon, Plus, ShieldQuestion, X } from '@vben/icons';
import {
Avatar,
Button,
Form,
Input,
Radio,
Select,
Tooltip,
} from 'ant-design-vue';
import { ImageUpload } from '#/components/upload';
import { UserSelectModal } from '#/components/user-select-modal';
import { DICT_TYPE, getBoolDictOptions, getIntDictOptions } from '#/utils';
const props = withDefaults(
defineProps<{
categoryList: BpmCategoryApi.CategoryVO[];
deptList: SystemDeptApi.Dept[];
userList: SystemUserApi.User[];
}>(),
{},
);
//
const formRef = ref();
//
const selectedStartUsers = ref<SystemUserApi.User[]>([]);
//
const selectedStartDepts = ref<SystemDeptApi.Dept[]>([]);
//
const selectedManagerUsers = ref<SystemUserApi.User[]>([]);
const userSelectFormRef = ref();
const currentSelectType = ref<'manager' | 'start'>('start');
//
const selectedUsers = ref<number[]>();
const rules: Record<string, Rule[]> = {
name: [{ required: true, message: '流程名称不能为空', trigger: 'blur' }],
key: [{ required: true, message: '流程标识不能为空', trigger: 'blur' }],
category: [{ required: true, message: '流程分类不能为空', trigger: 'blur' }],
type: [{ required: true, message: '流程类型不能为空', trigger: 'blur' }],
visible: [{ required: true, message: '是否可见不能为空', trigger: 'blur' }],
managerUserIds: [
{ required: true, message: '流程管理员不能为空', trigger: 'blur' },
],
};
//
const modelData = defineModel<any>();
//
watch(
() => modelData.value,
(newVal) => {
selectedStartUsers.value = newVal.startUserIds?.length
? (props.userList.filter((user: SystemUserApi.User) =>
newVal.startUserIds.includes(user.id),
) as SystemUserApi.User[])
: [];
selectedStartDepts.value = newVal.startDeptIds?.length
? (props.deptList.filter((dept: SystemDeptApi.Dept) =>
newVal.startDeptIds.includes(dept.id),
) as SystemDeptApi.Dept[])
: [];
selectedManagerUsers.value = newVal.managerUserIds?.length
? (props.userList.filter((user: SystemUserApi.User) =>
newVal.managerUserIds.includes(user.id),
) as SystemUserApi.User[])
: [];
},
{
immediate: true,
},
);
/** 打开发起人选择 */
const openStartUserSelect = () => {
currentSelectType.value = 'start';
selectedUsers.value = selectedStartUsers.value.map(
(user) => user.id,
) as number[];
userSelectFormRef.value.open(selectedUsers.value);
};
/** 打开部门选择 */
const openStartDeptSelect = () => {
// TODO
console.warn('部门选择功能暂未实现');
};
/** 打开管理员选择 */
const openManagerUserSelect = () => {
currentSelectType.value = 'manager';
selectedUsers.value = selectedManagerUsers.value.map(
(user) => user.id,
) as number[];
userSelectFormRef.value.open(selectedUsers.value);
};
/** 处理用户选择确认 */
const handleUserSelectConfirm = (userList: SystemUserApi.User[]) => {
modelData.value =
currentSelectType.value === 'start'
? {
...modelData.value,
startUserIds: userList.map((u) => u.id),
}
: {
...modelData.value,
managerUserIds: userList.map((u) => u.id),
};
};
/** 用户选择弹窗关闭 */
const handleUserSelectClosed = () => {
selectedUsers.value = [];
};
/** 用户选择弹窗取消 */
const handleUserSelectCancel = () => {
selectedUsers.value = [];
};
/** 处理发起人类型变化 */
const handleStartUserTypeChange = (value: SelectValue) => {
const numValue = Number(value);
switch (numValue) {
case 0: {
modelData.value = {
...modelData.value,
startUserIds: [],
startDeptIds: [],
};
break;
}
case 1: {
modelData.value = {
...modelData.value,
startDeptIds: [],
};
break;
}
case 2: {
modelData.value = {
...modelData.value,
startUserIds: [],
};
break;
}
}
};
/** 移除发起人 */
const handleRemoveStartUser = (user: SystemUserApi.User) => {
modelData.value = {
...modelData.value,
startUserIds: modelData.value.startUserIds.filter(
(id: number) => id !== user.id,
),
};
};
/** 移除部门 */
const handleRemoveStartDept = (dept: SystemDeptApi.Dept) => {
modelData.value = {
...modelData.value,
startDeptIds: modelData.value.startDeptIds.filter(
(id: number) => id !== dept.id,
),
};
};
/** 移除管理员 */
const handleRemoveManagerUser = (user: SystemUserApi.User) => {
modelData.value = {
...modelData.value,
managerUserIds: modelData.value.managerUserIds.filter(
(id: number) => id !== user.id,
),
};
};
/** 表单校验 */
const validate = async () => {
await formRef.value?.validate();
};
defineExpose({
validate,
});
</script>
<template>
<Form
ref="formRef"
:model="modelData"
:rules="rules"
:label-col="{ span: 6 }"
:wrapper-col="{ span: 18 }"
class="mt-5"
>
<Form.Item label="流程标识" name="key" class="mb-5">
<div class="flex items-center">
<Input
class="w-full"
v-model:value="modelData.key"
:disabled="!!modelData.id"
placeholder="请输入流程标识,以字母或下划线开头"
/>
<Tooltip
:title="
modelData.id ? '流程标识不可修改!' : '新建后,流程标识不可修改!'
"
placement="top"
>
<ShieldQuestion class="ml-1 text-gray-500" />
</Tooltip>
</div>
</Form.Item>
<Form.Item label="流程名称" name="name" class="mb-5">
<Input
v-model:value="modelData.name"
:disabled="!!modelData.id"
allow-clear
placeholder="请输入流程名称"
/>
</Form.Item>
<Form.Item label="流程分类" name="category" class="mb-5">
<Select
class="w-full"
v-model:value="modelData.category"
allow-clear
placeholder="请选择流程分类"
>
<Select.Option
v-for="category in categoryList"
:key="category.code"
:value="category.code"
>
{{ category.name }}
</Select.Option>
</Select>
</Form.Item>
<Form.Item label="流程图标" class="mb-5">
<ImageUpload v-model:value="modelData.icon" />
</Form.Item>
<Form.Item label="流程描述" name="description" class="mb-5">
<Input.TextArea v-model:value="modelData.description" allow-clear />
</Form.Item>
<Form.Item label="流程类型" name="type" class="mb-5">
<Radio.Group v-model:value="modelData.type">
<Radio
v-for="dict in getIntDictOptions(DICT_TYPE.BPM_MODEL_TYPE)"
:key="dict.value"
:value="dict.value"
>
{{ dict.label }}
</Radio>
</Radio.Group>
</Form.Item>
<Form.Item label="是否可见" name="visible" class="mb-5">
<Radio.Group v-model:value="modelData.visible">
<Radio
v-for="(dict, index) in getBoolDictOptions(
DICT_TYPE.INFRA_BOOLEAN_STRING,
)"
:key="index"
:value="dict.value"
>
{{ dict.label }}
</Radio>
</Radio.Group>
</Form.Item>
<Form.Item label="谁可以发起" name="startUserType" class="mb-5">
<Select
v-model:value="modelData.startUserType"
placeholder="请选择谁可以发起"
@change="handleStartUserTypeChange"
>
<Select.Option :value="0">全员</Select.Option>
<Select.Option :value="1">指定人员</Select.Option>
<Select.Option :value="2">指定部门</Select.Option>
</Select>
<div
v-if="modelData.startUserType === 1"
class="mt-2 flex flex-wrap gap-2"
>
<div
v-for="user in selectedStartUsers"
:key="user.id"
class="relative flex h-9 items-center rounded-full bg-gray-100 pr-2"
>
<Avatar
class="m-1"
:size="28"
v-if="user.avatar"
:src="user.avatar"
/>
<Avatar class="m-1" :size="28" v-else>
{{ user.nickname?.substring(0, 1) }}
</Avatar>
{{ user.nickname }}
<X
class="ml-2 size-4 cursor-pointer text-gray-400 hover:text-red-500"
@click="handleRemoveStartUser(user)"
/>
</div>
<Button
type="link"
@click="openStartUserSelect"
class="flex items-center"
>
<template #icon>
<IconifyIcon icon="mdi:account-plus-outline" class="size-[18px]" />
</template>
选择人员
</Button>
</div>
<div
v-if="modelData.startUserType === 2"
class="mt-2 flex flex-wrap gap-2"
>
<div
v-for="dept in selectedStartDepts"
:key="dept.id"
class="relative flex h-9 items-center rounded-full bg-gray-100 pr-2"
>
<IconifyIcon icon="mdi:building-outline" class="size-5" />
{{ dept.name }}
<X
class="ml-2 size-4 cursor-pointer text-gray-400 hover:text-red-500"
@click="handleRemoveStartDept(dept)"
/>
</div>
<Button
type="link"
@click="openStartDeptSelect"
class="flex items-center"
>
<template #icon>
<Plus class="size-[18px]" />
</template>
选择部门
</Button>
</div>
</Form.Item>
<Form.Item label="流程管理员" name="managerUserIds" class="mb-5">
<div class="flex flex-wrap gap-2">
<div
v-for="user in selectedManagerUsers"
:key="user.id"
class="relative flex h-9 items-center rounded-full bg-gray-100 pr-2"
>
<Avatar
class="m-1"
:size="28"
v-if="user.avatar"
:src="user.avatar"
/>
<Avatar class="m-1" :size="28" v-else>
{{ user.nickname?.substring(0, 1) }}
</Avatar>
{{ user.nickname }}
<X
class="ml-2 size-4 cursor-pointer text-gray-400 hover:text-red-500"
@click="handleRemoveManagerUser(user)"
/>
</div>
<Button
type="link"
@click="openManagerUserSelect"
class="flex items-center"
>
<template #icon>
<IconifyIcon icon="mdi:account-plus-outline" class="size-[18px]" />
</template>
选择人员
</Button>
</div>
</Form.Item>
</Form>
<!-- 用户选择弹窗 -->
<UserSelectModal
ref="userSelectFormRef"
v-model:value="selectedUsers"
:multiple="true"
title="选择用户"
@confirm="handleUserSelectConfirm"
@closed="handleUserSelectClosed"
@cancel="handleUserSelectCancel"
/>
</template>
<style lang="scss" scoped>
.bg-gray-100 {
background-color: #f5f7fa;
transition: all 0.3s;
&:hover {
background-color: #e6e8eb;
}
}
.upload-img-placeholder {
cursor: pointer;
background-color: #fafafa;
transition: all 0.3s;
&:hover {
border-color: #1890ff !important;
}
}
</style>

View File

@ -25,6 +25,7 @@ import {
updateCategorySortBatch,
} from '#/api/bpm/category';
import { getModelList } from '#/api/bpm/model';
import { router } from '#/router';
//
import CategoryForm from '../category/modules/form.vue';
@ -35,7 +36,6 @@ const [CategoryFormModal, categoryFormModalApi] = useVbenModal({
connectedComponent: CategoryForm,
destroyOnClose: true,
});
//
const modelListSpinning = refAutoReset(false, 3000);
//
@ -103,7 +103,9 @@ const handleQuery = () => {
/** 新增模型 */
const createModel = () => {
// TODO
router.push({
name: 'BpmModelCreate',
});
};
/** 处理下拉菜单命令 */
@ -160,6 +162,8 @@ const handleCategorySortSubmit = async () => {
<template>
<Page auto-content-height>
<!-- 流程分类表单弹窗 -->
<CategoryFormModal @success="getList" />
<Card
:body-style="{ padding: '10px' }"
class="mb-4"
@ -249,7 +253,4 @@ const handleCategorySortSubmit = async () => {
</div>
</Card>
</Page>
<!-- 流程分类表单弹窗 -->
<CategoryFormModal @success="getList" />
</template>