Merge pull request !214 from xingyu/dev
pull/219/MERGE
xingyu 2025-09-19 08:20:33 +00:00 committed by Gitee
commit 45a5c9bc8e
No known key found for this signature in database
GPG Key ID: 173E9B9CA92EEF8F
1033 changed files with 27105 additions and 31820 deletions

View File

@ -21,8 +21,7 @@
// CSS
"vunguyentuan.vscode-css-variables",
// package.json PNPM catalog
"antfu.pnpm-catalog-lens",
"augment.vscode-augment"
"antfu.pnpm-catalog-lens"
],
"unwantedRecommendations": [
// volar

View File

@ -9,7 +9,7 @@
## 🐶 新手必读
- nodejs > 20.10.0 && pnpm > 10.10.0 (强制使用pnpm)
- nodejs > 20.10.0 && pnpm > 10.14.0 (强制使用pnpm)
- 演示地址【Vue3 + element-plus】<http://dashboard-vue3.yudao.iocoder.cn>
- 演示地址【Vue3 + vben5(ant-design-vue)】:<http://dashboard-vben.yudao.iocoder.cn>
- 演示地址【Vue2 + element-ui】<http://dashboard.yudao.iocoder.cn>
@ -35,7 +35,7 @@
## 外包项目请联系【非项目需求请勿扫码,非客服,不解答项目问题】
![alt 定制开发](.image/wx-xingyu.png)
![alt 软件定制开发 数舵科技](.image/wx-xingyu.png)
## 技术栈

View File

@ -46,16 +46,22 @@
"@vueuse/core": "catalog:",
"@vueuse/integrations": "catalog:",
"ant-design-vue": "catalog:",
"bpmn-js": "catalog:",
"bpmn-js-properties-panel": "catalog:",
"bpmn-js-token-simulation": "catalog:",
"camunda-bpmn-moddle": "catalog:",
"cropperjs": "catalog:",
"dayjs": "catalog:",
"diagram-js": "catalog:",
"fast-xml-parser": "catalog:",
"highlight.js": "catalog:",
"min-dash": "catalog:",
"pinia": "catalog:",
"steady-xml": "catalog:",
"tinymce": "catalog:",
"vue": "catalog:",
"vue-dompurify-html": "catalog:",
"vue-router": "catalog:",
"vue3-signature": "catalog:"
},
"devDependencies": {
"@types/crypto-js": "catalog:"
}
}

View File

@ -22,6 +22,9 @@ const AutoComplete = defineAsyncComponent(
() => import('ant-design-vue/es/auto-complete'),
);
const Button = defineAsyncComponent(() => import('ant-design-vue/es/button'));
const Cascader = defineAsyncComponent(
() => import('ant-design-vue/es/cascader'),
);
const Checkbox = defineAsyncComponent(
() => import('ant-design-vue/es/checkbox'),
);
@ -59,6 +62,9 @@ const Textarea = defineAsyncComponent(() =>
const TimePicker = defineAsyncComponent(
() => import('ant-design-vue/es/time-picker'),
);
const TimeRangePicker = defineAsyncComponent(() =>
import('ant-design-vue/es/time-picker').then((res) => res.TimeRangePicker),
);
const TreeSelect = defineAsyncComponent(
() => import('ant-design-vue/es/tree-select'),
);
@ -100,6 +106,7 @@ const withDefaultPlaceholder = <T extends Component>(
// 这里需要自行根据业务组件库进行适配,需要用到的组件都需要在这里类型说明
export type ComponentType =
| 'ApiCascader'
| 'ApiSelect'
| 'ApiTreeSelect'
| 'AutoComplete'
@ -126,6 +133,7 @@ export type ComponentType =
| 'Switch'
| 'Textarea'
| 'TimePicker'
| 'TimeRangePicker'
| 'TreeSelect'
| 'Upload'
| BaseFormComponentType;
@ -135,6 +143,21 @@ async function initComponentAdapter() {
// 如果你的组件体积比较大,可以使用异步加载
// Button: () =>
// import('xxx').then((res) => res.Button),
ApiCascader: withDefaultPlaceholder(
{
...ApiComponent,
name: 'ApiCascader',
},
'select',
{
component: Cascader,
fieldNames: { label: 'label', value: 'value', children: 'children' },
loadingSlot: 'suffixIcon',
modelPropName: 'value',
optionsPropName: 'treeData',
visibleEvent: 'onVisibleChange',
},
),
ApiSelect: withDefaultPlaceholder(
{
...ApiComponent,
@ -195,6 +218,7 @@ async function initComponentAdapter() {
Textarea: withDefaultPlaceholder(Textarea, 'input'),
RichTextarea,
TimePicker,
TimeRangePicker,
TreeSelect: withDefaultPlaceholder(TreeSelect, 'select'),
Upload,
FileUpload,

View File

@ -7,9 +7,7 @@ import type { ComponentType } from './component';
import { setupVbenForm, useVbenForm as useForm, z } from '@vben/common-ui';
import { $t } from '@vben/locales';
/** 手机号正则表达式(中国) */
const MOBILE_REGEX = /(?:0|86|\+86)?1[3-9]\d{9}/;
import { isMobile } from '@vben/utils';
async function initSetupVbenForm() {
setupVbenForm<ComponentType>({
@ -44,7 +42,7 @@ async function initSetupVbenForm() {
mobile: (value, _params, ctx) => {
if (value === undefined || value === null || value.length === 0) {
return true;
} else if (!MOBILE_REGEX.test(value)) {
} else if (!isMobile(value)) {
return $t('ui.formRules.mobile', [ctx.label]);
}
return true;
@ -54,7 +52,7 @@ async function initSetupVbenForm() {
if (value === undefined || value === null || value.length === 0) {
return $t('ui.formRules.required', [ctx.label]);
}
if (!MOBILE_REGEX.test(value)) {
if (!isMobile(value)) {
return $t('ui.formRules.mobile', [ctx.label]);
}
return true;

View File

@ -1,79 +0,0 @@
/* 来自 @vben/plugins/vxe-table style.css覆盖 vxe-table 原有的样式定义,使用 vben 的样式主题 */
:root {
--vxe-ui-font-color: hsl(var(--foreground));
--vxe-ui-font-primary-color: hsl(var(--primary));
/* --vxe-ui-font-lighten-color: #babdc0;
--vxe-ui-font-darken-color: #86898e; */
--vxe-ui-font-disabled-color: hsl(var(--foreground) / 50%);
/* base */
--vxe-ui-base-popup-border-color: hsl(var(--border));
--vxe-ui-input-disabled-color: hsl(var(--border) / 60%);
/* --vxe-ui-base-popup-box-shadow: 0px 12px 30px 8px rgb(0 0 0 / 50%); */
/* layout */
--vxe-ui-layout-background-color: hsl(var(--background));
--vxe-ui-table-resizable-line-color: hsl(var(--heavy));
/* --vxe-ui-table-fixed-left-scrolling-box-shadow: 8px 0px 10px -5px hsl(var(--accent));
--vxe-ui-table-fixed-right-scrolling-box-shadow: -8px 0px 10px -5px hsl(var(--accent)); */
/* input */
--vxe-ui-input-border-color: hsl(var(--border));
/* --vxe-ui-input-placeholder-color: #8d9095; */
/* --vxe-ui-input-disabled-background-color: #262727; */
/* loading */
--vxe-ui-loading-background-color: hsl(var(--overlay-content));
/* table */
--vxe-ui-table-header-background-color: hsl(var(--accent));
--vxe-ui-table-border-color: hsl(var(--border));
--vxe-ui-table-row-hover-background-color: hsl(var(--accent-hover));
--vxe-ui-table-row-striped-background-color: hsl(var(--accent) / 60%);
--vxe-ui-table-row-hover-striped-background-color: hsl(var(--accent));
--vxe-ui-table-row-radio-checked-background-color: hsl(var(--accent));
--vxe-ui-table-row-hover-radio-checked-background-color: hsl(
var(--accent-hover)
);
--vxe-ui-table-row-checkbox-checked-background-color: hsl(var(--accent));
--vxe-ui-table-row-hover-checkbox-checked-background-color: hsl(
var(--accent-hover)
);
--vxe-ui-table-row-current-background-color: hsl(var(--accent));
--vxe-ui-table-row-hover-current-background-color: hsl(var(--accent-hover));
--vxe-ui-font-primary-tinge-color: hsl(var(--primary));
--vxe-ui-font-primary-lighten-color: hsl(var(--primary) / 60%);
--vxe-ui-font-primary-darken-color: hsl(var(--primary));
/* height: auto !important; */
/* --vxe-ui-table-fixed-scrolling-box-shadow-color: rgb(0 0 0 / 80%); */
}
.vxe-tools--operate {
margin-right: 0.25rem;
margin-left: 0.75rem;
}
.vxe-table-custom--checkbox-option:hover {
background: none !important;
}
.vxe-toolbar {
padding: 0;
}
.vxe-buttons--wrapper:not(:empty),
.vxe-tools--operate:not(:empty),
.vxe-tools--wrapper:not(:empty) {
padding: 0.6em 0;
}
.vxe-tools--operate:not(:has(button)) {
margin-left: 0;
}

View File

@ -6,7 +6,8 @@ import { h } from 'vue';
import { IconifyIcon } from '@vben/icons';
import { $te } from '@vben/locales';
import {
AsyncComponents,
AsyncVxeColumn,
AsyncVxeTable,
createRequiredValidation,
setupVbenVxeTable,
useVbenVxeGrid,
@ -34,8 +35,6 @@ import { $t } from '#/locales';
import { useVbenForm } from './form';
import '#/adapter/style.css';
setupVbenVxeTable({
configVxeTable: (vxeUI) => {
vxeUI.setConfig({
@ -357,16 +356,8 @@ setupVbenVxeTable({
export { createRequiredValidation, useVbenVxeGrid };
const [VxeTable, VxeColumn, VxeToolbar] = AsyncComponents;
export { VxeColumn, VxeTable, VxeToolbar };
export const [VxeTable, VxeColumn] = [AsyncVxeTable, AsyncVxeColumn];
// add by 芋艿from https://github.com/vbenjs/vue-vben-admin/blob/main/playground/src/adapter/vxe-table.ts#L264-L270
export type OnActionClickParams<T = Recordable<any>> = {
code: string;
row: T;
};
export type OnActionClickFn<T = Recordable<any>> = (
params: OnActionClickParams<T>,
) => void;
export * from '#/components/table-action';
export type * from '@vben/plugins/vxe-table';

View File

@ -12,6 +12,7 @@ export namespace AiImageApi {
// AI 绘图
export interface Image {
id: number; // 编号
userId: number;
platform: string; // 平台
model: string; // 模型
prompt: string; // 提示词

View File

@ -1,7 +1,6 @@
import type { AiWriteTypeEnum } from '@vben/constants';
import type { PageParam, PageResult } from '@vben/request';
import type { AiWriteTypeEnum } from '#/utils';
import { useAppConfig } from '@vben/hooks';
import { fetchEventSource } from '@vben/request';
import { useAccessStore } from '@vben/stores';

View File

@ -1,57 +0,0 @@
import type { PageParam, PageResult } from '@vben/request';
import { requestClient } from '#/api/request';
export namespace ProductUnitApi {
/** 产品单位信息 */
export interface ProductUnit {
id: number; // 编号
groupId?: number; // 分组编号
name?: string; // 单位名称
basic?: number; // 基础单位
number?: number; // 单位数量/相对于基础单位
usageType: number; // 用途
}
}
/** 查询产品单位分页 */
export function getProductUnitPage(params: PageParam) {
return requestClient.get<PageResult<ProductUnitApi.ProductUnit>>(
'/basic/product-unit/page',
{ params },
);
}
/** 查询产品单位详情 */
export function getProductUnit(id: number) {
return requestClient.get<ProductUnitApi.ProductUnit>(
`/basic/product-unit/get?id=${id}`,
);
}
/** 新增产品单位 */
export function createProductUnit(data: ProductUnitApi.ProductUnit) {
return requestClient.post('/basic/product-unit/create', data);
}
/** 修改产品单位 */
export function updateProductUnit(data: ProductUnitApi.ProductUnit) {
return requestClient.put('/basic/product-unit/update', data);
}
/** 删除产品单位 */
export function deleteProductUnit(id: number) {
return requestClient.delete(`/basic/product-unit/delete?id=${id}`);
}
/** 批量删除产品单位 */
export function deleteProductUnitListByIds(ids: number[]) {
return requestClient.delete(
`/basic/product-unit/delete-list?ids=${ids.join(',')}`,
);
}
/** 导出产品单位 */
export function exportProductUnit(params: any) {
return requestClient.download('/basic/product-unit/export-excel', params);
}

View File

@ -1,61 +0,0 @@
import type { PageParam, PageResult } from '@vben/request';
import { requestClient } from '#/api/request';
export namespace ProductUnitGroupApi {
/** 产品单位组信息 */
export interface ProductUnitGroup {
id: number; // 编号
name?: string; // 产品单位组名称
status?: number; // 开启状态
}
}
/** 查询产品单位组分页 */
export function getProductUnitGroupPage(params: PageParam) {
return requestClient.get<PageResult<ProductUnitGroupApi.ProductUnitGroup>>(
'/basic/product-unit-group/page',
{ params },
);
}
/** 查询产品单位组详情 */
export function getProductUnitGroup(id: number) {
return requestClient.get<ProductUnitGroupApi.ProductUnitGroup>(
`/basic/product-unit-group/get?id=${id}`,
);
}
/** 新增产品单位组 */
export function createProductUnitGroup(
data: ProductUnitGroupApi.ProductUnitGroup,
) {
return requestClient.post('/basic/product-unit-group/create', data);
}
/** 修改产品单位组 */
export function updateProductUnitGroup(
data: ProductUnitGroupApi.ProductUnitGroup,
) {
return requestClient.put('/basic/product-unit-group/update', data);
}
/** 删除产品单位组 */
export function deleteProductUnitGroup(id: number) {
return requestClient.delete(`/basic/product-unit-group/delete?id=${id}`);
}
/** 批量删除产品单位组 */
export function deleteProductUnitGroupListByIds(ids: number[]) {
return requestClient.delete(
`/basic/product-unit-group/delete-list?ids=${ids.join(',')}`,
);
}
/** 导出产品单位组 */
export function exportProductUnitGroup(params: any) {
return requestClient.download(
'/basic/product-unit-group/export-excel',
params,
);
}

View File

@ -49,5 +49,7 @@ export async function deleteProcessExpression(id: number) {
/** 导出流程表达式 */
export async function exportProcessExpression(params: any) {
return requestClient.download('/bpm/process-expression/export-excel', params);
return requestClient.download('/bpm/process-expression/export-excel', {
params,
});
}

View File

@ -1,9 +1,12 @@
import type {
BpmCandidateStrategyEnum,
BpmNodeTypeEnum,
} from '@vben/constants';
import type { PageParam, PageResult } from '@vben/request';
import type { BpmTaskApi } from '../task';
import type { BpmModelApi } from '#/api/bpm/model';
import type { BpmCandidateStrategyEnum, BpmNodeTypeEnum } from '#/utils';
import { requestClient } from '#/api/request';

View File

@ -133,5 +133,7 @@ export const getChildrenTaskList = async (id: string) => {
// 撤回任务
export const withdrawTask = async (taskId: string) => {
return await requestClient.put('/bpm/task/withdraw', null, { params: { taskId } });
return await requestClient.put('/bpm/task/withdraw', null, {
params: { taskId },
});
};

View File

@ -108,7 +108,7 @@ export function deleteBusiness(id: number) {
/** 导出商机 */
export function exportBusiness(params: any) {
return requestClient.download('/crm/business/export-excel', params);
return requestClient.download('/crm/business/export-excel', { params });
}
/** 联系人关联商机列表 */

View File

@ -67,7 +67,7 @@ export function deleteClue(id: number) {
/** 导出线索 */
export function exportClue(params: any) {
return requestClient.download('/crm/clue/export-excel', params);
return requestClient.download('/crm/clue/export-excel', { params });
}
/** 线索转移 */

View File

@ -96,7 +96,7 @@ export function deleteContact(id: number) {
/** 导出联系人 */
export function exportContact(params: any) {
return requestClient.download('/crm/contact/export-excel', params);
return requestClient.download('/crm/contact/export-excel', { params });
}
/** 获得联系人列表(精简) */

View File

@ -108,7 +108,7 @@ export function deleteContract(id: number) {
/** 导出合同 */
export function exportContract(params: any) {
return requestClient.download('/crm/contract/export-excel', params);
return requestClient.download('/crm/contract/export-excel', { params });
}
/** 提交审核 */

View File

@ -74,7 +74,7 @@ export function deleteCustomer(id: number) {
/** 导出客户 */
export function exportCustomer(params: any) {
return requestClient.download('/crm/customer/export-excel', params);
return requestClient.download('/crm/customer/export-excel', { params });
}
/** 下载客户导入模板 */

View File

@ -53,5 +53,5 @@ export function deleteProduct(id: number) {
/** 导出产品 */
export function exportProduct(params: any) {
return requestClient.download('/crm/product/export-excel', params);
return requestClient.download('/crm/product/export-excel', { params });
}

View File

@ -89,7 +89,7 @@ export function deleteReceivable(id: number) {
/** 导出回款 */
export function exportReceivable(params: any) {
return requestClient.download('/crm/receivable/export-excel', params);
return requestClient.download('/crm/receivable/export-excel', { params });
}
/** 提交审核 */

View File

@ -50,6 +50,7 @@ export namespace ErpPurchaseInApi {
status?: number;
}
// TODO @nehcupdatePurchaseInStatus 是不是需要?
/** 采购入库状态更新参数 */
export interface PurchaseInStatusParams {
id: number;

View File

@ -24,7 +24,6 @@ export namespace ErpSaleOrderApi {
depositPrice?: number; // 定金金额,单位:元
items?: SaleOrderItem[]; // 销售订单产品明细列表
}
/** ERP 销售订单产品明细 */
export interface SaleOrderItem {
id?: number; // 订单项编号
orderId?: number; // 采购订单编号

View File

@ -17,7 +17,6 @@ export namespace ErpStockCheckApi {
creatorName?: string; // 创建人
items?: StockCheckItem[]; // 盘点产品清单
}
// 库存盘点单产品信息
export interface StockCheckItem {
id?: number; // 编号
warehouseId?: number; // 仓库编号
@ -33,6 +32,7 @@ export namespace ErpStockCheckApi {
stockCount?: number; // 账面库存
remark?: string; // 备注
}
/** 库存盘点单分页查询参数 */
export interface StockCheckPageParams extends PageParam {
no?: string;

View File

@ -71,7 +71,7 @@ export namespace InfraCodegenApi {
}
/** 创建代码生成请求 */
export interface CodegenCreateListReq {
export interface CodegenCreateListReqVO {
dataSourceConfigId?: number;
tableNames: string[];
}
@ -136,7 +136,9 @@ export function getSchemaTableList(params: any) {
}
/** 基于数据库的表结构,创建代码生成器的表定义 */
export function createCodegenList(data: InfraCodegenApi.CodegenCreateListReq) {
export function createCodegenList(
data: InfraCodegenApi.CodegenCreateListReqVO,
) {
return requestClient.post('/infra/codegen/create-list', data);
}

View File

@ -55,5 +55,7 @@ export function deleteDemo01ContactList(ids: number[]) {
/** 导出示例联系人 */
export function exportDemo01Contact(params: any) {
return requestClient.download('/infra/demo01-contact/export-excel', params);
return requestClient.download('/infra/demo01-contact/export-excel', {
params,
});
}

View File

@ -42,5 +42,7 @@ export function deleteDemo02Category(id: number) {
/** 导出示例分类 */
export function exportDemo02Category(params: any) {
return requestClient.download('/infra/demo02-category/export-excel', params);
return requestClient.download('/infra/demo02-category/export-excel', {
params,
});
}

View File

@ -70,10 +70,9 @@ export function deleteDemo03StudentList(ids: number[]) {
/** 导出学生 */
export function exportDemo03Student(params: any) {
return requestClient.download(
'/infra/demo03-student-erp/export-excel',
return requestClient.download('/infra/demo03-student-erp/export-excel', {
params,
);
});
}
// ==================== 子表(学生课程) ====================

View File

@ -72,10 +72,9 @@ export function deleteDemo03StudentList(ids: number[]) {
/** 导出学生 */
export function exportDemo03Student(params: any) {
return requestClient.download(
'/infra/demo03-student-inner/export-excel',
return requestClient.download('/infra/demo03-student-inner/export-excel', {
params,
);
});
}
// ==================== 子表(学生课程) ====================

View File

@ -72,10 +72,9 @@ export function deleteDemo03StudentList(ids: number[]) {
/** 导出学生 */
export function exportDemo03Student(params: any) {
return requestClient.download(
'/infra/demo03-student-normal/export-excel',
return requestClient.download('/infra/demo03-student-normal/export-excel', {
params,
);
});
}
// ==================== 子表(学生课程) ====================

View File

@ -113,6 +113,8 @@ export namespace MallSpuApi {
createTime?: Date;
/** 商品状态 */
status?: number;
/** 浏览量 */
browseCount?: number;
}
/** 商品状态更新 */

View File

@ -31,6 +31,8 @@ export namespace MallDeliveryPickUpStoreApi {
status: number;
/** 绑定用户编号组数 */
verifyUserIds: number[];
/** 营业时间 用于fieldMappingTime */
rangeTime: any[];
}
/** 绑定自提店员请求 */

View File

@ -16,6 +16,7 @@ export namespace PayAppApi {
merchantId: number;
merchantName: string;
createTime?: Date;
channelCodes: string[];
}
/** 更新状态请求 */

View File

@ -19,7 +19,7 @@ export namespace SystemMailTemplateApi {
}
/** 邮件发送信息 */
export interface MailSendReq {
export interface MailSendReqVO {
toMails: string[];
ccMails?: string[];
bccMails?: string[];
@ -66,6 +66,6 @@ export function deleteMailTemplateList(ids: number[]) {
}
/** 发送邮件 */
export function sendMail(data: SystemMailTemplateApi.MailSendReq) {
export function sendMail(data: SystemMailTemplateApi.MailSendReqVO) {
return requestClient.post('/system/mail-template/send-mail', data);
}

View File

@ -17,7 +17,7 @@ export namespace SystemNotifyTemplateApi {
}
/** 发送站内信请求 */
export interface NotifySendReq {
export interface NotifySendReqVO {
userId: number;
userType: number;
templateCode: string;
@ -74,6 +74,6 @@ export function exportNotifyTemplate(params: any) {
}
/** 发送站内信 */
export function sendNotify(data: SystemNotifyTemplateApi.NotifySendReq) {
export function sendNotify(data: SystemNotifyTemplateApi.NotifySendReqVO) {
return requestClient.post('/system/notify-template/send-notify', data);
}

View File

@ -55,3 +55,10 @@ export function updateOAuth2Client(data: SystemOAuth2ClientApi.OAuth2Client) {
export function deleteOAuth2Client(id: number) {
return requestClient.delete(`/system/oauth2-client/delete?id=${id}`);
}
/** 批量删除 OAuth2.0 客户端 */
export function deleteOAuth2ClientList(ids: number[]) {
return requestClient.delete(
`/system/oauth2-client/delete-list?ids=${ids.join(',')}`,
);
}

View File

@ -20,7 +20,7 @@ export namespace SystemSmsTemplateApi {
}
/** 发送短信请求 */
export interface SmsSendReq {
export interface SmsSendReqVO {
mobile: string;
templateCode: string;
templateParams: Record<string, any>;
@ -72,6 +72,6 @@ export function exportSmsTemplate(params: any) {
}
/** 发送短信 */
export function sendSms(data: SystemSmsTemplateApi.SmsSendReq) {
export function sendSms(data: SystemSmsTemplateApi.SmsSendReqVO) {
return requestClient.post('/system/sms-template/send-sms', data);
}

View File

@ -46,3 +46,10 @@ export function updateSocialClient(data: SystemSocialClientApi.SocialClient) {
export function deleteSocialClient(id: number) {
return requestClient.delete(`/system/social-client/delete?id=${id}`);
}
/** 批量删除社交客户端 */
export function deleteSocialClientList(ids: number[]) {
return requestClient.delete(
`/system/social-client/delete-list?ids=${ids.join(',')}`,
);
}

View File

@ -56,7 +56,7 @@ export function deleteUserList(ids: number[]) {
/** 导出用户 */
export function exportUser(params: any) {
return requestClient.download('/system/user/export-excel', params);
return requestClient.download('/system/user/export-excel', { params });
}
/** 下载用户导入模板 */

View File

@ -1,73 +0,0 @@
<script setup lang="ts">
import type { Item } from './ui/typing';
import { onMounted, onUnmounted, ref } from 'vue';
import { Tinyflow as TinyflowNative } from './ui/index';
import './ui/index.css';
const props = defineProps<{
className?: string;
data?: Record<string, any>;
provider?: {
internal?: () => Item[] | Promise<Item[]>;
knowledge?: () => Item[] | Promise<Item[]>;
llm?: () => Item[] | Promise<Item[]>;
};
style?: Record<string, string>;
}>();
const divRef = ref<HTMLDivElement | null>(null);
let tinyflow: null | TinyflowNative = null;
// provider
const defaultProvider = {
llm: () => [] as Item[],
knowledge: () => [] as Item[],
internal: () => [] as Item[],
};
onMounted(() => {
if (divRef.value) {
// provider props.provider
const mergedProvider = {
...defaultProvider,
...props.provider,
};
tinyflow = new TinyflowNative({
element: divRef.value as Element,
data: props.data || {},
provider: mergedProvider,
});
}
});
onUnmounted(() => {
if (tinyflow) {
tinyflow.destroy();
tinyflow = null;
}
});
const getData = () => {
if (tinyflow) {
return tinyflow.getData();
}
console.warn('Tinyflow instance is not initialized');
return null;
};
defineExpose({
getData,
});
</script>
<template>
<div
ref="divRef"
class="tinyflow"
:class="[className]"
:style="style"
style="height: 100%"
></div>
</template>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@ -1,68 +0,0 @@
export interface Item {
children?: Item[];
label: string;
value: number | string;
}
export interface Position {
x: number;
y: number;
}
export interface Viewport {
x: number;
y: number;
zoom: number;
}
export interface Node {
data?: Record<string, any>;
draggable?: boolean;
height?: number;
id: string;
position: Position;
selected?: boolean;
type?: string;
width?: number;
}
export interface Edge {
animated?: boolean;
id: string;
label?: string;
source: string;
target: string;
type?: string;
}
export type TinyflowData = Partial<{
edges: Edge[];
nodes: Node[];
viewport: Viewport;
}>;
export interface TinyflowOptions {
data?: TinyflowData;
element: Element | string;
provider?: {
internal?: () => Item[] | Promise<Item[]>;
knowledge?: () => Item[] | Promise<Item[]>;
llm?: () => Item[] | Promise<Item[]>;
};
}
export declare class Tinyflow {
private _init;
private _setOptions;
private options;
private rootEl;
private svelteFlowInstance;
constructor(options: TinyflowOptions);
destroy(): void;
getData(): {
edges: Edge[];
nodes: Node[];
viewport: Viewport;
};
getOptions(): TinyflowOptions;
setData(data: TinyflowData): void;
}

View File

@ -0,0 +1,685 @@
<script lang="ts" setup>
// import 'bpmn-js/dist/assets/diagram-js.css' //
// import 'bpmn-js/dist/assets/bpmn-font/css/bpmn.css'
// import 'bpmn-js/dist/assets/bpmn-font/css/bpmn-codes.css'
// import 'bpmn-js/dist/assets/bpmn-font/css/bpmn-embedded.css'
// import 'bpmn-js-properties-panel/dist/assets/bpmn-js-properties-panel.css' //
import {
computed,
defineEmits,
defineOptions,
defineProps,
h,
onBeforeUnmount,
onMounted,
provide,
ref,
} from 'vue';
import {
AlignLeftOutlined,
ApiOutlined,
DownloadOutlined,
EyeOutlined,
FolderOpenOutlined,
RedoOutlined,
ReloadOutlined,
UndoOutlined,
WarningOutlined,
ZoomInOutlined,
ZoomOutOutlined,
} from '@vben/icons';
import { Button, ButtonGroup, message, Modal, Tooltip } from 'ant-design-vue';
//
// @ts-ignore
import tokenSimulation from 'bpmn-js-token-simulation';
import BpmnModeler from 'bpmn-js/lib/Modeler';
//
// import hljs from 'highlight.js/lib/highlight'
// import 'highlight.js/styles/github-gist.css'
// hljs.registerLanguage('xml', 'highlight.js/lib/languages/xml')
// hljs.registerLanguage('json', 'highlight.js/lib/languages/json')
// const eventName = reactive({
// name: ''
// })
import hljs from 'highlight.js'; //
// json
// import xml2js from 'xml-js'
// import xml2js from 'fast-xml-parser'
import { parseXmlString, XmlNode } from 'steady-xml';
import DefaultEmptyXML from './plugins/defaultEmpty';
import activitiModdleDescriptor from './plugins/descriptor/activitiDescriptor.json';
//
// import bpmnPropertiesProvider from "bpmn-js-properties-panel/lib/provider/bpmn";
// import propertiesPanelModule from 'bpmn-js-properties-panel'
// import propertiesProviderModule from 'bpmn-js-properties-panel/lib/provider/camunda'
// Moddle
import camundaModdleDescriptor from './plugins/descriptor/camundaDescriptor.json';
import flowableModdleDescriptor from './plugins/descriptor/flowableDescriptor.json';
import activitiModdleExtension from './plugins/extension-moddle/activiti';
// Extension
import camundaModdleExtension from './plugins/extension-moddle/camunda';
import flowableModdleExtension from './plugins/extension-moddle/flowable';
//
import customTranslate from './plugins/translate/customTranslate';
import translationsCN from './plugins/translate/zh';
import 'highlight.js/styles/github.css';
defineOptions({ name: 'MyProcessDesigner' });
const props = defineProps({
value: { type: String, default: '' }, // xml
// valueWatch: true, // xml watch
processId: { type: String, default: '' }, // key
processName: { type: String, default: '' }, // name
formId: { type: Number, default: undefined }, // form
translations: {
//
type: Object,
default: () => {},
},
// eslint-disable-next-line vue/require-default-prop
additionalModel: [Object, Array], // model
moddleExtension: {
// moddle
type: Object,
default: () => {},
},
onlyCustomizeAddi: {
type: Boolean,
default: false,
},
onlyCustomizeModdle: {
type: Boolean,
default: false,
},
simulation: {
type: Boolean,
default: true,
},
keyboard: {
type: Boolean,
default: true,
},
prefix: {
type: String,
default: 'camunda',
},
events: {
type: Array,
default: () => ['element.click'],
},
headerButtonSize: {
type: String,
default: 'small',
validator: (value: string) =>
['default', 'medium', 'mini', 'small'].includes(value),
},
headerButtonType: {
type: String,
default: 'primary',
validator: (value: string) =>
['danger', 'default', 'info', 'primary', 'success', 'warning'].includes(
value,
),
},
});
//
const emit = defineEmits([
'destroy',
'init-finished',
'save',
'commandStack-changed',
'input',
'change',
'canvas-viewbox-changed',
// eventName.name
'element-click',
]);
const bpmnCanvas = ref();
const refFile = ref();
/**
* 代码高亮
*/
const highlightedCode = (code: string) => {
//
if (previewType.value === 'json') {
code = JSON.stringify(code, null, 2);
}
const result = hljs.highlight(code, {
language: previewType.value,
ignoreIllegals: true,
});
return result.value || '&nbsp;';
};
provide('configGlobal', props);
let bpmnModeler: any = null;
const defaultZoom = ref(1);
const previewModelVisible = ref(false);
const simulationStatus = ref(false);
const previewResult = ref('');
const previewType = ref('xml');
const recoverable = ref(false);
const revocable = ref(false);
const additionalModules = computed(() => {
// console.log(props.additionalModel, 'additionalModel');
const Modules: any[] = [];
//
if (props.onlyCustomizeAddi) {
if (
Object.prototype.toString.call(props.additionalModel) === '[object Array]'
) {
return props.additionalModel || [];
}
return [props.additionalModel];
}
//
if (
Object.prototype.toString.call(props.additionalModel) === '[object Array]'
) {
Modules.push(...(props.additionalModel as any[]));
} else {
props.additionalModel && Modules.push(props.additionalModel);
}
//
const TranslateModule = {
translate: ['value', customTranslate(props.translations || translationsCN)],
};
Modules.push(TranslateModule);
//
if (props.simulation) {
Modules.push(tokenSimulation);
}
//
// if (this.prefix === "bpmn") {
// Modules.push(bpmnModdleExtension);
// }
// console.log(props.prefix, 'props.prefix ');
if (props.prefix === 'camunda') {
Modules.push(camundaModdleExtension);
}
if (props.prefix === 'flowable') {
Modules.push(flowableModdleExtension);
}
if (props.prefix === 'activiti') {
Modules.push(activitiModdleExtension);
}
return Modules;
});
const moddleExtensions = computed(() => {
// console.log(props.onlyCustomizeModdle, 'props.onlyCustomizeModdle');
// console.log(props.moddleExtension, 'props.moddleExtension');
// console.log(props.prefix, 'props.prefix');
const Extensions: any = {};
// 使
if (props.onlyCustomizeModdle) {
return props.moddleExtension || null;
}
//
if (props.moddleExtension) {
for (const key in props.moddleExtension) {
Extensions[key] = props.moddleExtension[key];
}
}
// ""
if (props.prefix === 'activiti') {
Extensions.activiti = activitiModdleDescriptor;
}
if (props.prefix === 'flowable') {
Extensions.flowable = flowableModdleDescriptor;
}
if (props.prefix === 'camunda') {
Extensions.camunda = camundaModdleDescriptor;
}
return Extensions;
});
// console.log(additionalModules, 'additionalModules()');
// console.log(moddleExtensions, 'moddleExtensions()');
const initBpmnModeler = () => {
if (bpmnModeler) return;
const data: any = document.querySelector('#bpmnCanvas');
// console.log(data, 'data');
// console.log(props.keyboard, 'props.keyboard');
// console.log(additionalModules, 'additionalModules()');
// console.log(moddleExtensions, 'moddleExtensions()');
bpmnModeler = new BpmnModeler({
// container: this.$refs['bpmn-canvas'],
// container: getCurrentInstance(),
// container: needClass,
// container: bpmnCanvas.value,
container: data,
// width: '100%',
//
// propertiesPanel: {
// parent: '#js-properties-panel'
// },
keyboard: props.keyboard ? { bindTo: document } : null,
// additionalModules: additionalModules.value,
additionalModules: additionalModules.value as any[],
moddleExtensions: moddleExtensions.value,
// additionalModules: [
// additionalModules.value
// propertiesPanelModule,
// propertiesProviderModule
// propertiesProviderModule
// ],
// moddleExtensions: { camunda: moddleExtensions.value }
});
// bpmnModeler.createDiagram()
// console.log(bpmnModeler, 'bpmnModeler111111')
// eslint-disable-next-line vue/custom-event-name-casing
emit('init-finished', bpmnModeler);
initModelListeners();
};
const initModelListeners = () => {
const EventBus = bpmnModeler.get('eventBus');
// console.log(EventBus, 'EventBus');
// , . - ,
props.events.forEach((event: any) => {
EventBus.on(event, (eventObj: any) => {
// const eventName = event.replaceAll('.', '-');
// eventName.name = eventName
const element = eventObj ? eventObj.element : null;
// console.log(eventName, 'eventName');
// console.log(element, 'element');
// eslint-disable-next-line vue/custom-event-name-casing
emit('element-click', element, eventObj);
// emit(eventName, element, eventObj)
});
});
// xml
EventBus.on('commandStack.changed', async (event: any) => {
try {
recoverable.value = bpmnModeler.get('commandStack').canRedo();
revocable.value = bpmnModeler.get('commandStack').canUndo();
const { xml } = await bpmnModeler.saveXML({ format: true });
// eslint-disable-next-line vue/custom-event-name-casing
emit('commandStack-changed', event);
emit('input', xml);
emit('change', xml);
emit('save', xml);
} catch {
// console.error(`[Process Designer Warn]: ${e.message || e}`);
}
});
//
bpmnModeler.on('canvas.viewbox.changed', ({ viewbox }: { viewbox: any }) => {
// eslint-disable-next-line vue/custom-event-name-casing
emit('canvas-viewbox-changed', { viewbox });
const { scale } = viewbox;
defaultZoom.value = Math.floor(scale * 100) / 100;
});
};
/* 创建新的流程图 */
const createNewDiagram = async (xml: any) => {
// console.log(xml, 'xml');
//
const newId = props.processId || `Process_${Date.now()}`;
const newName = props.processName || `业务流程_${Date.now()}`;
const xmlString = xml || DefaultEmptyXML(newId, newName, props.prefix);
try {
// console.log(xmlString, 'xmlString')
// console.log(this.bpmnModeler.importXML);
const { warnings } = await bpmnModeler.importXML(xmlString);
// console.log(warnings, 'warnings');
if (warnings && warnings.length > 0) {
// warnings.forEach((warn: any) => console.warn(warn));
}
} catch {
// console.error(`[Process Designer Warn]: ${e.message || e}`);
}
};
//
const downloadProcess = async (type: string) => {
try {
//
if (type === 'xml' || type === 'bpmn') {
const { err, xml } = await bpmnModeler.saveXML();
//
if (err) {
// console.error(`[Process Designer Warn ]: ${err.message || err}`);
}
const { href, filename } = setEncoded(type.toUpperCase(), xml);
downloadFunc(href, filename);
} else {
const { err, svg } = await bpmnModeler.saveSVG();
//
if (err) {
// return console.error(err);
}
const { href, filename } = setEncoded('SVG', svg);
downloadFunc(href, filename);
}
} catch (error: any) {
console.error(`[Process Designer Warn ]: ${error.message || error}`);
}
//
function downloadFunc(href: string, filename: string) {
if (href && filename) {
const a = document.createElement('a');
a.download = filename; //
a.href = href; // URL
a.click(); //
URL.revokeObjectURL(a.href); // URL
}
}
};
//
const setEncoded = (type: string, data: string) => {
const filename = 'diagram';
const encodedData = encodeURIComponent(data);
return {
filename: `${filename}.${type}`,
href: `data:application/${
type === 'svg' ? 'text/xml' : 'bpmn20-xml'
};charset=UTF-8,${encodedData}`,
data,
};
};
//
const importLocalFile = () => {
const file = refFile.value.files[0];
const reader = new FileReader();
// eslint-disable-next-line unicorn/prefer-blob-reading-methods
reader.readAsText(file);
reader.addEventListener('load', function () {
const xmlStr = this.result;
createNewDiagram(xmlStr);
emit('save', xmlStr);
});
};
/* ------------------------------------------------ refs methods ------------------------------------------------------ */
const downloadProcessAsXml = () => {
downloadProcess('xml');
};
const downloadProcessAsBpmn = () => {
downloadProcess('bpmn');
};
const downloadProcessAsSvg = () => {
downloadProcess('svg');
};
const processSimulation = () => {
simulationStatus.value = !simulationStatus.value;
// console.log(
// bpmnModeler.get('toggleMode', 'strict'),
// "bpmnModeler.get('toggleMode')",
// );
props.simulation && bpmnModeler.get('toggleMode', 'strict').toggleMode();
};
const processRedo = () => {
bpmnModeler.get('commandStack').redo();
};
const processUndo = () => {
bpmnModeler.get('commandStack').undo();
};
const processZoomIn = (zoomStep = 0.1) => {
const newZoom = Math.floor(defaultZoom.value * 100 + zoomStep * 100) / 100;
if (newZoom > 4) {
throw new Error(
'[Process Designer Warn ]: The zoom ratio cannot be greater than 4',
);
}
defaultZoom.value = newZoom;
bpmnModeler.get('canvas').zoom(defaultZoom.value);
};
const processZoomOut = (zoomStep = 0.1) => {
const newZoom = Math.floor(defaultZoom.value * 100 - zoomStep * 100) / 100;
if (newZoom < 0.2) {
throw new Error(
'[Process Designer Warn ]: The zoom ratio cannot be less than 0.2',
);
}
defaultZoom.value = newZoom;
bpmnModeler.get('canvas').zoom(defaultZoom.value);
};
const processReZoom = () => {
defaultZoom.value = 1;
bpmnModeler.get('canvas').zoom('fit-viewport', 'auto');
};
const processRestart = () => {
recoverable.value = false;
revocable.value = false;
createNewDiagram(null);
};
const elementsAlign = (align: string) => {
const Align = bpmnModeler.get('alignElements');
const Selection = bpmnModeler.get('selection');
const SelectedElements = Selection.get();
if (!SelectedElements || SelectedElements.length <= 1) {
message.warning('请按住 Shift 键选择多个元素对齐');
// alert(' Ctrl
return;
}
Modal.confirm({
title: '警告',
content: '自动对齐可能造成图形变形,是否继续?',
okText: '确定',
cancelText: '取消',
icon: h(WarningOutlined) as any,
onOk() {
Align.trigger(SelectedElements, align);
},
});
};
/* ----------------------------- 方法结束 ---------------------------------*/
const previewProcessXML = () => {
// console.log(bpmnModeler.saveXML, 'bpmnModeler');
bpmnModeler.saveXML({ format: true }).then(({ xml }: { xml: string }) => {
// console.log(xml, 'xml111111')
previewResult.value = xml;
previewType.value = 'xml';
previewModelVisible.value = true;
});
};
const previewProcessJson = () => {
bpmnModeler.saveXML({ format: true }).then(({ xml }: { xml: string }) => {
const rootNodes = new XmlNode('root' as any, parseXmlString(xml));
previewResult.value = rootNodes.parent?.toJSON() as unknown as string;
previewType.value = 'json';
previewModelVisible.value = true;
});
};
/* ------------------------------------------------ 芋道源码 methods ------------------------------------------------------ */
onMounted(() => {
initBpmnModeler();
createNewDiagram(props.value);
});
onBeforeUnmount(() => {
if (bpmnModeler) bpmnModeler.destroy();
emit('destroy', bpmnModeler);
bpmnModeler = null;
});
</script>
<template>
<div class="my-process-designer">
<div
class="my-process-designer__header"
style="z-index: 999; display: table-row-group"
>
<slot name="control-header"></slot>
<template v-if="!$slots['control-header']">
<ButtonGroup key="file-control">
<Button
:icon="h(FolderOpenOutlined)"
title="打开文件"
@click="refFile.click()"
/>
<Tooltip placement="bottom">
<template #title>
<div>
<Button type="link" @click="downloadProcessAsXml()">
下载为XML文件
</Button>
<br />
<Button type="link" @click="downloadProcessAsSvg()">
下载为SVG文件
</Button>
<br />
<Button type="link" @click="downloadProcessAsBpmn()">
下载为BPMN文件
</Button>
</div>
</template>
<Button :icon="h(DownloadOutlined)" title="下载文件" />
</Tooltip>
<Tooltip>
<template #title>
<Button type="link" @click="previewProcessXML">XML</Button>
<br />
<Button type="link" @click="previewProcessJson">JSON</Button>
</template>
<Button :icon="h(EyeOutlined)" title="浏览" />
</Tooltip>
<Tooltip
v-if="props.simulation"
:title="simulationStatus ? '退出模拟' : '开启模拟'"
>
<Button
:icon="h(ApiOutlined)"
title="模拟"
@click="processSimulation"
/>
</Tooltip>
</ButtonGroup>
<ButtonGroup key="align-control">
<Tooltip title="向左对齐">
<Button
:icon="h(AlignLeftOutlined)"
class="align align-bottom"
@click="elementsAlign('left')"
/>
</Tooltip>
<Tooltip title="向右对齐">
<Button
:icon="h(AlignLeftOutlined)"
class="align align-top"
@click="elementsAlign('right')"
/>
</Tooltip>
<Tooltip title="向上对齐">
<Button
:icon="h(AlignLeftOutlined)"
class="align align-left"
@click="elementsAlign('top')"
/>
</Tooltip>
<Tooltip title="向下对齐">
<Button
:icon="h(AlignLeftOutlined)"
class="align align-right"
@click="elementsAlign('bottom')"
/>
</Tooltip>
<Tooltip title="水平居中">
<Button
:icon="h(AlignLeftOutlined)"
class="align align-center"
@click="elementsAlign('center')"
/>
</Tooltip>
<Tooltip title="垂直居中">
<Button
:icon="h(AlignLeftOutlined)"
class="align align-middle"
@click="elementsAlign('middle')"
/>
</Tooltip>
</ButtonGroup>
<ButtonGroup key="scale-control">
<Tooltip title="缩小视图">
<Button
:icon="h(ZoomOutOutlined)"
@click="processZoomOut()"
:disabled="defaultZoom < 0.2"
/>
</Tooltip>
<Button>{{ `${Math.floor(defaultZoom * 10 * 10)}%` }}</Button>
<Tooltip title="放大视图">
<Button
:icon="h(ZoomInOutlined)"
@click="processZoomIn()"
:disabled="defaultZoom > 4"
/>
</Tooltip>
<Tooltip title="重置视图并居中">
<Button :icon="h(ReloadOutlined)" @click="processReZoom()" />
</Tooltip>
</ButtonGroup>
<ButtonGroup key="stack-control">
<Tooltip title="撤销">
<Button
:icon="h(UndoOutlined)"
@click="processUndo()"
:disabled="!revocable"
/>
</Tooltip>
<Tooltip title="恢复">
<Button
:icon="h(RedoOutlined)"
@click="processRedo()"
:disabled="!recoverable"
/>
</Tooltip>
<Tooltip title="重新绘制">
<Button :icon="h(ReloadOutlined)" @click="processRestart()" />
</Tooltip>
</ButtonGroup>
</template>
<!-- 用于打开本地文件-->
<input
type="file"
id="files"
ref="refFile"
style="display: none"
accept=".xml, .bpmn"
@change="importLocalFile"
/>
</div>
<div class="my-process-designer__container">
<div
class="my-process-designer__canvas"
ref="bpmnCanvas"
id="bpmnCanvas"
style="width: 1680px; height: 800px"
></div>
<!-- <div id="js-properties-panel" class="panel"></div> -->
<!-- <div class="my-process-designer__canvas" ref="bpmn-canvas"></div> -->
</div>
<Dialog
title="预览"
v-model:open="previewModelVisible"
width="80%"
:scroll="true"
style="max-height: 600px"
>
<div>
<pre><code v-dompurify-html="highlightedCode(previewResult)" class="hljs"></code></pre>
</div>
</Dialog>
</div>
</template>

View File

@ -0,0 +1,417 @@
<script lang="ts" setup>
import { defineProps, h, onBeforeUnmount, onMounted, ref, watch } from 'vue';
import { BpmProcessInstanceStatus, DICT_TYPE } from '@vben/constants';
import { UndoOutlined, ZoomInOutlined, ZoomOutOutlined } from '@vben/icons';
import { dateFormatter, formatPast2 } from '@vben/utils';
import { Button, ButtonGroup, Modal, Row, Table } from 'ant-design-vue';
import BpmnViewer from 'bpmn-js/lib/Viewer';
import MoveCanvasModule from 'diagram-js/lib/navigation/movecanvas';
import { DictTag } from '#/components/dict-tag';
import '../theme/index.scss';
const props = defineProps({
xml: {
type: String,
required: true,
},
view: {
type: Object,
require: true,
default: () => ({}),
},
});
const processCanvas = ref();
const bpmnViewer = ref<any | BpmnViewer>(null);
const customDefs = ref();
const defaultZoom = ref(1); //
const isLoading = ref(false); //
const processInstance = ref<any>({}); //
const tasks = ref([]); //
const dialogVisible = ref(false); //
const dialogTitle = ref<string | undefined>(undefined); //
const selectActivityType = ref<string | undefined>(undefined); // Task
const selectTasks = ref<any[]>([]); //
/** Zoom恢复 */
const processReZoom = () => {
defaultZoom.value = 1;
bpmnViewer.value?.get('canvas').zoom('fit-viewport', 'auto');
};
/** Zoom放大 */
const processZoomIn = (zoomStep = 0.1) => {
const newZoom = Math.floor(defaultZoom.value * 100 + zoomStep * 100) / 100;
if (newZoom > 4) {
throw new Error(
'[Process Designer Warn ]: The zoom ratio cannot be greater than 4',
);
}
defaultZoom.value = newZoom;
bpmnViewer.value?.get('canvas').zoom(defaultZoom.value);
};
/** Zoom缩小 */
const processZoomOut = (zoomStep = 0.1) => {
const newZoom = Math.floor(defaultZoom.value * 100 - zoomStep * 100) / 100;
if (newZoom < 0.2) {
throw new Error(
'[Process Designer Warn ]: The zoom ratio cannot be less than 0.2',
);
}
defaultZoom.value = newZoom;
bpmnViewer.value?.get('canvas').zoom(defaultZoom.value);
};
/** 流程图预览清空 */
const clearViewer = () => {
if (processCanvas.value) {
processCanvas.value.innerHTML = '';
}
if (bpmnViewer.value) {
bpmnViewer.value.destroy();
}
bpmnViewer.value = null;
};
/** 添加自定义箭头 */
// TODO marker-endmarker-start
const addCustomDefs = () => {
if (!bpmnViewer.value) {
return;
}
const canvas = bpmnViewer.value?.get('canvas');
const svg = canvas?._svg;
svg.append(customDefs.value);
};
/** 节点选中 */
const onSelectElement = (element: any) => {
//
selectActivityType.value = undefined;
dialogTitle.value = undefined;
if (!element || !processInstance.value?.id) {
return;
}
// UserTask
const activityType = element.type;
selectActivityType.value = activityType;
if (activityType === 'bpmn:UserTask') {
dialogTitle.value = element.businessObject
? element.businessObject.name
: undefined;
selectTasks.value = tasks.value.filter(
(item: any) => item?.taskDefinitionKey === element.id,
);
dialogVisible.value = true;
} else if (
activityType === 'bpmn:EndEvent' ||
activityType === 'bpmn:StartEvent'
) {
dialogTitle.value = '审批信息';
selectTasks.value = [
{
assigneeUser: processInstance.value.startUser,
createTime: processInstance.value.startTime,
endTime: processInstance.value.endTime,
status: processInstance.value.status,
durationInMillis: processInstance.value.durationInMillis,
},
];
dialogVisible.value = true;
}
};
/** 初始化 BPMN 视图 */
const importXML = async (xml: string) => {
//
clearViewer();
//
if (xml !== null && xml !== '') {
try {
bpmnViewer.value = new BpmnViewer({
additionalModules: [MoveCanvasModule],
container: processCanvas.value,
});
//
bpmnViewer.value.on('element.click', ({ element }: { element: any }) => {
onSelectElement(element);
});
// BPMN
isLoading.value = true;
await bpmnViewer.value.importXML(xml);
//
addCustomDefs();
} catch {
clearViewer();
} finally {
isLoading.value = false;
//
setProcessStatus(props.view);
}
}
};
/** 高亮流程 */
const setProcessStatus = (view: any) => {
//
if (!view || !view.processInstance) {
return;
}
processInstance.value = view.processInstance;
tasks.value = view.tasks;
if (isLoading.value || !bpmnViewer.value) {
return;
}
const {
unfinishedTaskActivityIds,
finishedTaskActivityIds,
finishedSequenceFlowActivityIds,
rejectedTaskActivityIds,
} = view;
const canvas: any = bpmnViewer.value.get('canvas');
const elementRegistry: any = bpmnViewer.value.get('elementRegistry');
//
if (Array.isArray(finishedSequenceFlowActivityIds)) {
finishedSequenceFlowActivityIds.forEach((item: any) => {
if (item !== null) {
canvas.addMarker(item, 'success');
const element = elementRegistry.get(item);
const conditionExpression = element.businessObject.conditionExpression;
if (conditionExpression) {
canvas.addMarker(item, 'condition-expression');
}
}
});
}
if (Array.isArray(finishedTaskActivityIds)) {
finishedTaskActivityIds.forEach((item: any) =>
canvas.addMarker(item, 'success'),
);
}
//
if (Array.isArray(unfinishedTaskActivityIds)) {
unfinishedTaskActivityIds.forEach((item: any) =>
canvas.addMarker(item, 'primary'),
);
}
//
if (Array.isArray(rejectedTaskActivityIds)) {
rejectedTaskActivityIds.forEach((item: any) => {
if (item !== null) {
canvas.addMarker(item, 'danger');
}
});
}
// end end finishedTaskActivityIds
if (
[BpmProcessInstanceStatus.CANCEL, BpmProcessInstanceStatus.REJECT].includes(
processInstance.value.status,
)
) {
const endNodes = elementRegistry.filter(
(element: any) => element.type === 'bpmn:EndEvent',
);
endNodes.forEach((item: any) => {
canvas.removeMarker(item.id, 'success');
if (processInstance.value.status === BpmProcessInstanceStatus.CANCEL) {
canvas.addMarker(item.id, 'cancel');
} else {
canvas.addMarker(item.id, 'danger');
}
});
}
};
watch(
() => props.xml,
(newXml) => {
importXML(newXml);
},
{ immediate: true },
);
watch(
() => props.view,
(newView) => {
setProcessStatus(newView);
},
{ immediate: true },
);
/** mounted初始化 */
onMounted(() => {
importXML(props.xml);
setProcessStatus(props.view);
});
/** unmount销毁 */
onBeforeUnmount(() => {
clearViewer();
});
</script>
<template>
<div class="process-viewer">
<div style="height: 100%" ref="processCanvas" v-show="!isLoading"></div>
<!-- 自定义箭头样式用于已完成状态下流程连线箭头 -->
<defs ref="customDefs">
<marker
id="sequenceflow-end-white-success"
viewBox="0 0 20 20"
refX="11"
refY="10"
markerWidth="10"
markerHeight="10"
orient="auto"
>
<path
class="success-arrow"
d="M 1 5 L 11 10 L 1 15 Z"
style="
stroke-width: 1px;
stroke-linecap: round;
stroke-dasharray: 10000, 1;
"
/>
</marker>
<marker
id="conditional-flow-marker-white-success"
viewBox="0 0 20 20"
refX="-1"
refY="10"
markerWidth="10"
markerHeight="10"
orient="auto"
>
<path
class="success-conditional"
d="M 0 10 L 8 6 L 16 10 L 8 14 Z"
style="
stroke-width: 1px;
stroke-linecap: round;
stroke-dasharray: 10000, 1;
"
/>
</marker>
</defs>
<!-- 审批记录 -->
<Modal
:title="dialogTitle || '审批记录'"
v-model:open="dialogVisible"
:width="1000"
>
<Row>
<Table :data-source="selectTasks" size="small" :bordered="true">
<Table.Column title="序号" align="center" width="50">
<template #default="{ index }">
{{ index + 1 }}
</template>
</Table.Column>
<Table.Column
title="审批人"
width="100"
align="center"
v-if="selectActivityType === 'bpmn:UserTask'"
>
<template #default="{ record }">
{{ record.assigneeUser?.nickname || record.ownerUser?.nickname }}
</template>
</Table.Column>
<Table.Column
title="发起人"
data-index="assigneeUser.nickname"
width="100"
align="center"
v-else
/>
<Table.Column title="部门" width="100" align="center">
<template #default="{ record }">
{{ record.assigneeUser?.deptName || record.ownerUser?.deptName }}
</template>
</Table.Column>
<Table.Column
:custom-render="({ text }) => dateFormatter(text)"
align="center"
title="开始时间"
data-index="createTime"
width="140"
/>
<Table.Column
:custom-render="({ text }) => dateFormatter(text)"
align="center"
title="结束时间"
data-index="endTime"
width="140"
/>
<Table.Column
align="center"
title="审批状态"
data-index="status"
width="90"
>
<template #default="{ record }">
<DictTag
:type="DICT_TYPE.BPM_TASK_STATUS"
:value="record.status"
/>
</template>
</Table.Column>
<Table.Column
align="center"
title="审批建议"
data-index="reason"
width="120"
v-if="selectActivityType === 'bpmn:UserTask'"
/>
<Table.Column
align="center"
title="耗时"
data-index="durationInMillis"
width="100"
>
<template #default="{ record }">
{{ formatPast2(record.durationInMillis) }}
</template>
</Table.Column>
</Table>
</Row>
</Modal>
<!-- Zoom放大缩小 -->
<div style="position: absolute; top: 0; left: 0; width: 100%">
<Row justify="end">
<ButtonGroup key="scale-control">
<Button
:disabled="defaultZoom <= 0.3"
:icon="h(ZoomOutOutlined)"
@click="processZoomOut()"
/>
<Button style="width: 90px">
{{ `${Math.floor(defaultZoom * 10 * 10)}%` }}
</Button>
<Button
:disabled="defaultZoom >= 3.9"
:icon="h(ZoomInOutlined)"
@click="processZoomIn()"
/>
<Button :icon="h(UndoOutlined)" @click="processReZoom()" />
</ButtonGroup>
</Row>
</div>
</div>
</template>

View File

@ -0,0 +1,8 @@
import MyProcessDesigner from './ProcessDesigner.vue';
MyProcessDesigner.install = function (Vue: any) {
Vue.component(MyProcessDesigner.name, MyProcessDesigner);
};
// 流程图的设计器,可编辑
export default MyProcessDesigner;

View File

@ -0,0 +1,8 @@
import MyProcessViewer from './ProcessViewer.vue';
MyProcessViewer.install = function (Vue: any) {
Vue.component(MyProcessViewer.name, MyProcessViewer);
};
// 流程图的查看器,不可编辑
export default MyProcessViewer;

View File

@ -0,0 +1,440 @@
import { getChildLanes } from 'bpmn-js/lib/features/modeling/util/LaneUtil';
import { isAny } from 'bpmn-js/lib/features/modeling/util/ModelingUtil';
import { isEventSubProcess, isExpanded } from 'bpmn-js/lib/util/DiUtil';
import { is } from 'bpmn-js/lib/util/ModelUtil';
import { hasPrimaryModifier } from 'diagram-js/lib/util/Mouse';
import { assign, forEach, isArray } from 'min-dash';
/**
* A provider for BPMN 2.0 elements context pad
*/
export default function ContextPadProvider(
config,
injector,
eventBus,
contextPad,
modeling,
elementFactory,
connect,
create,
popupMenu,
canvas,
rules,
translate,
) {
config = config || {};
contextPad.registerProvider(this);
this._contextPad = contextPad;
this._modeling = modeling;
this._elementFactory = elementFactory;
this._connect = connect;
this._create = create;
this._popupMenu = popupMenu;
this._canvas = canvas;
this._rules = rules;
this._translate = translate;
if (config.autoPlace !== false) {
this._autoPlace = injector.get('autoPlace', false);
}
eventBus.on('create.end', 250, (event) => {
const context = event.context;
const shape = context.shape;
if (!hasPrimaryModifier(event) || !contextPad.isOpen(shape)) {
return;
}
const entries = contextPad.getEntries(shape);
if (entries.replace) {
entries.replace.action.click(event, shape);
}
});
}
ContextPadProvider.$inject = [
'config.contextPad',
'injector',
'eventBus',
'contextPad',
'modeling',
'elementFactory',
'connect',
'create',
'popupMenu',
'canvas',
'rules',
'translate',
'elementRegistry',
];
ContextPadProvider.prototype.getContextPadEntries = function (element) {
const autoPlace = this._autoPlace;
const canvas = this._canvas;
const connect = this._connect;
const contextPad = this._contextPad;
const create = this._create;
const elementFactory = this._elementFactory;
const modeling = this._modeling;
const popupMenu = this._popupMenu;
const rules = this._rules;
const translate = this._translate;
const actions = {};
if (element.type === 'label') {
return actions;
}
const businessObject = element.businessObject;
function startConnect(event, element) {
connect.start(event, element);
}
function removeElement() {
modeling.removeElements([element]);
}
function getReplaceMenuPosition(element) {
const Y_OFFSET = 5;
const diagramContainer = canvas.getContainer();
const pad = contextPad.getPad(element).html;
const diagramRect = diagramContainer.getBoundingClientRect();
const padRect = pad.getBoundingClientRect();
const top = padRect.top - diagramRect.top;
const left = padRect.left - diagramRect.left;
const pos = {
x: left,
y: top + padRect.height + Y_OFFSET,
};
return pos;
}
/**
* Create an append action
*
* @param {string} type
* @param {string} className
* @param {string} [title]
* @param {object} [options]
*
* @return {object} descriptor
*/
function appendAction(type, className, title, options) {
if (typeof title !== 'string') {
options = title;
title = translate('Append {type}', { type: type.replace(/^bpmn:/, '') });
}
function appendStart(event, element) {
const shape = elementFactory.createShape(assign({ type }, options));
create.start(event, shape, {
source: element,
});
}
const append = autoPlace
? function (event, element) {
const shape = elementFactory.createShape(assign({ type }, options));
autoPlace.append(element, shape);
}
: appendStart;
return {
group: 'model',
className,
title,
action: {
dragstart: appendStart,
click: append,
},
};
}
function splitLaneHandler(count) {
return function (event, element) {
// actual split
modeling.splitLane(element, count);
// refresh context pad after split to
// get rid of split icons
contextPad.open(element, true);
};
}
if (
isAny(businessObject, ['bpmn:Lane', 'bpmn:Participant']) &&
isExpanded(businessObject)
) {
const childLanes = getChildLanes(element);
assign(actions, {
'lane-insert-above': {
group: 'lane-insert-above',
className: 'bpmn-icon-lane-insert-above',
title: translate('Add Lane above'),
action: {
click(event, element) {
modeling.addLane(element, 'top');
},
},
},
});
if (childLanes.length < 2) {
if (element.height >= 120) {
assign(actions, {
'lane-divide-two': {
group: 'lane-divide',
className: 'bpmn-icon-lane-divide-two',
title: translate('Divide into two Lanes'),
action: {
click: splitLaneHandler(2),
},
},
});
}
if (element.height >= 180) {
assign(actions, {
'lane-divide-three': {
group: 'lane-divide',
className: 'bpmn-icon-lane-divide-three',
title: translate('Divide into three Lanes'),
action: {
click: splitLaneHandler(3),
},
},
});
}
}
assign(actions, {
'lane-insert-below': {
group: 'lane-insert-below',
className: 'bpmn-icon-lane-insert-below',
title: translate('Add Lane below'),
action: {
click(event, element) {
modeling.addLane(element, 'bottom');
},
},
},
});
}
if (is(businessObject, 'bpmn:FlowNode')) {
if (is(businessObject, 'bpmn:EventBasedGateway')) {
assign(actions, {
'append.receive-task': appendAction(
'bpmn:ReceiveTask',
'bpmn-icon-receive-task',
translate('Append ReceiveTask'),
),
'append.message-intermediate-event': appendAction(
'bpmn:IntermediateCatchEvent',
'bpmn-icon-intermediate-event-catch-message',
translate('Append MessageIntermediateCatchEvent'),
{ eventDefinitionType: 'bpmn:MessageEventDefinition' },
),
'append.timer-intermediate-event': appendAction(
'bpmn:IntermediateCatchEvent',
'bpmn-icon-intermediate-event-catch-timer',
translate('Append TimerIntermediateCatchEvent'),
{ eventDefinitionType: 'bpmn:TimerEventDefinition' },
),
'append.condition-intermediate-event': appendAction(
'bpmn:IntermediateCatchEvent',
'bpmn-icon-intermediate-event-catch-condition',
translate('Append ConditionIntermediateCatchEvent'),
{ eventDefinitionType: 'bpmn:ConditionalEventDefinition' },
),
'append.signal-intermediate-event': appendAction(
'bpmn:IntermediateCatchEvent',
'bpmn-icon-intermediate-event-catch-signal',
translate('Append SignalIntermediateCatchEvent'),
{ eventDefinitionType: 'bpmn:SignalEventDefinition' },
),
});
} else if (
isEventType(
businessObject,
'bpmn:BoundaryEvent',
'bpmn:CompensateEventDefinition',
)
) {
assign(actions, {
'append.compensation-activity': appendAction(
'bpmn:Task',
'bpmn-icon-task',
translate('Append compensation activity'),
{
isForCompensation: true,
},
),
});
} else if (
!is(businessObject, 'bpmn:EndEvent') &&
!businessObject.isForCompensation &&
!isEventType(
businessObject,
'bpmn:IntermediateThrowEvent',
'bpmn:LinkEventDefinition',
) &&
!isEventSubProcess(businessObject)
) {
assign(actions, {
'append.end-event': appendAction(
'bpmn:EndEvent',
'bpmn-icon-end-event-none',
translate('Append EndEvent'),
),
'append.gateway': appendAction(
'bpmn:ExclusiveGateway',
'bpmn-icon-gateway-none',
translate('Append Gateway'),
),
'append.append-task': appendAction(
'bpmn:UserTask',
'bpmn-icon-user-task',
translate('Append Task'),
),
'append.intermediate-event': appendAction(
'bpmn:IntermediateThrowEvent',
'bpmn-icon-intermediate-event-none',
translate('Append Intermediate/Boundary Event'),
),
});
}
}
if (!popupMenu.isEmpty(element, 'bpmn-replace')) {
// Replace menu entry
assign(actions, {
replace: {
group: 'edit',
className: 'bpmn-icon-screw-wrench',
title: '修改类型',
action: {
click(event, element) {
const position = assign(getReplaceMenuPosition(element), {
cursor: { x: event.x, y: event.y },
});
popupMenu.open(element, 'bpmn-replace', position);
},
},
},
});
}
if (
isAny(businessObject, [
'bpmn:FlowNode',
'bpmn:InteractionNode',
'bpmn:DataObjectReference',
'bpmn:DataStoreReference',
])
) {
assign(actions, {
'append.text-annotation': appendAction(
'bpmn:TextAnnotation',
'bpmn-icon-text-annotation',
),
connect: {
group: 'connect',
className: 'bpmn-icon-connection-multi',
title: translate(
`Connect using ${
businessObject.isForCompensation ? '' : 'Sequence/MessageFlow or '
}Association`,
),
action: {
click: startConnect,
dragstart: startConnect,
},
},
});
}
if (
isAny(businessObject, [
'bpmn:DataObjectReference',
'bpmn:DataStoreReference',
])
) {
assign(actions, {
connect: {
group: 'connect',
className: 'bpmn-icon-connection-multi',
title: translate('Connect using DataInputAssociation'),
action: {
click: startConnect,
dragstart: startConnect,
},
},
});
}
if (is(businessObject, 'bpmn:Group')) {
assign(actions, {
'append.text-annotation': appendAction(
'bpmn:TextAnnotation',
'bpmn-icon-text-annotation',
),
});
}
// delete element entry, only show if allowed by rules
let deleteAllowed = rules.allowed('elements.delete', { elements: [element] });
if (isArray(deleteAllowed)) {
// was the element returned as a deletion candidate?
deleteAllowed = deleteAllowed[0] === element;
}
if (deleteAllowed) {
assign(actions, {
delete: {
group: 'edit',
className: 'bpmn-icon-trash',
title: translate('Remove'),
action: {
click: removeElement,
},
},
});
}
return actions;
};
// helpers /////////
function isEventType(eventBo, type, definition) {
const isType = eventBo.$instanceOf(type);
let isDefinition = false;
const definitions = eventBo.eventDefinitions || [];
forEach(definitions, (def) => {
if (def.$type === definition) {
isDefinition = true;
}
});
return isType && isDefinition;
}

View File

@ -0,0 +1,6 @@
import CustomContextPadProvider from './contentPadProvider';
export default {
__init__: ['contextPadProvider'],
contextPadProvider: ['type', CustomContextPadProvider],
};

View File

@ -0,0 +1,24 @@
export default (key, name, type) => {
if (!type) type = 'camunda';
const TYPE_TARGET = {
activiti: 'http://activiti.org/bpmn',
camunda: 'http://bpmn.io/schema/bpmn',
flowable: 'http://flowable.org/bpmn',
};
return `<?xml version="1.0" encoding="UTF-8"?>
<bpmn2:definitions
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:bpmn2="http://www.omg.org/spec/BPMN/20100524/MODEL"
xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI"
xmlns:dc="http://www.omg.org/spec/DD/20100524/DC"
xmlns:di="http://www.omg.org/spec/DD/20100524/DI"
id="diagram_${key}"
targetNamespace="${TYPE_TARGET[type]}">
<bpmn2:process id="${key}" name="${name}" isExecutable="true">
</bpmn2:process>
<bpmndi:BPMNDiagram id="BPMNDiagram_1">
<bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="${key}">
</bpmndi:BPMNPlane>
</bpmndi:BPMNDiagram>
</bpmn2:definitions>`;
};

View File

@ -0,0 +1,101 @@
'use strict';
import { some } from 'min-dash';
// const some = require('min-dash').some
// const some = some
const ALLOWED_TYPES = {
FailedJobRetryTimeCycle: [
'bpmn:StartEvent',
'bpmn:BoundaryEvent',
'bpmn:IntermediateCatchEvent',
'bpmn:Activity',
],
Connector: ['bpmn:EndEvent', 'bpmn:IntermediateThrowEvent'],
Field: ['bpmn:EndEvent', 'bpmn:IntermediateThrowEvent'],
};
function is(element, type) {
return (
element &&
typeof element.$instanceOf === 'function' &&
element.$instanceOf(type)
);
}
function exists(element) {
return element && element.length;
}
function includesType(collection, type) {
return (
exists(collection) &&
some(collection, (element) => {
return is(element, type);
})
);
}
function anyType(element, types) {
return some(types, (type) => {
return is(element, type);
});
}
function isAllowed(propName, propDescriptor, newElement) {
const name = propDescriptor.name;
const types = ALLOWED_TYPES[name.replace(/activiti:/, '')];
return name === propName && anyType(newElement, types);
}
function ActivitiModdleExtension(eventBus) {
eventBus.on(
'property.clone',
function (context) {
const newElement = context.newElement;
const propDescriptor = context.propertyDescriptor;
this.canCloneProperty(newElement, propDescriptor);
},
this,
);
}
ActivitiModdleExtension.$inject = ['eventBus'];
ActivitiModdleExtension.prototype.canCloneProperty = function (
newElement,
propDescriptor,
) {
if (
isAllowed('activiti:FailedJobRetryTimeCycle', propDescriptor, newElement)
) {
return (
includesType(newElement.eventDefinitions, 'bpmn:TimerEventDefinition') ||
includesType(newElement.eventDefinitions, 'bpmn:SignalEventDefinition') ||
is(
newElement.loopCharacteristics,
'bpmn:MultiInstanceLoopCharacteristics',
)
);
}
if (isAllowed('activiti:Connector', propDescriptor, newElement)) {
return includesType(
newElement.eventDefinitions,
'bpmn:MessageEventDefinition',
);
}
if (isAllowed('activiti:Field', propDescriptor, newElement)) {
return includesType(
newElement.eventDefinitions,
'bpmn:MessageEventDefinition',
);
}
};
// module.exports = ActivitiModdleExtension;
export default ActivitiModdleExtension;

View File

@ -0,0 +1,11 @@
/*
* @author igdianov
* address https://github.com/igdianov/activiti-bpmn-moddle
* */
import activitiExtension from './activitiExtension';
export default {
__init__: ['ActivitiModdleExtension'],
ActivitiModdleExtension: ['type', activitiExtension],
};

View File

@ -0,0 +1,165 @@
'use strict';
import { isFunction, isObject, some } from 'min-dash';
// const isFunction = isFunction,
// isObject = isObject,
// some = some
// const isFunction = require('min-dash').isFunction,
// isObject = require('min-dash').isObject,
// some = require('min-dash').some
const WILDCARD = '*';
function CamundaModdleExtension(eventBus) {
// eslint-disable-next-line @typescript-eslint/no-this-alias
const self = this;
eventBus.on('moddleCopy.canCopyProperty', (context) => {
const parent = context.parent;
const property = context.property;
return self.canCopyProperty(property, parent);
});
}
CamundaModdleExtension.$inject = ['eventBus'];
/**
* Check wether to disallow copying property.
*/
CamundaModdleExtension.prototype.canCopyProperty = function (property, parent) {
// (1) check wether property is allowed in parent
if (isObject(property) && !isAllowedInParent(property, parent)) {
return false;
}
// (2) check more complex scenarios
if (is(property, 'camunda:InputOutput') && !this.canHostInputOutput(parent)) {
return false;
}
if (
isAny(property, ['camunda:Connector', 'camunda:Field']) &&
!this.canHostConnector(parent)
) {
return false;
}
if (is(property, 'camunda:In') && !this.canHostIn(parent)) {
return false;
}
};
CamundaModdleExtension.prototype.canHostInputOutput = function (parent) {
// allowed in camunda:Connector
const connector = getParent(parent, 'camunda:Connector');
if (connector) {
return true;
}
// special rules inside bpmn:FlowNode
const flowNode = getParent(parent, 'bpmn:FlowNode');
if (!flowNode) {
return false;
}
if (
isAny(flowNode, ['bpmn:StartEvent', 'bpmn:Gateway', 'bpmn:BoundaryEvent'])
) {
return false;
}
return !(is(flowNode, 'bpmn:SubProcess') && flowNode.get('triggeredByEvent'));
};
CamundaModdleExtension.prototype.canHostConnector = function (parent) {
const serviceTaskLike = getParent(parent, 'camunda:ServiceTaskLike');
if (is(serviceTaskLike, 'bpmn:MessageEventDefinition')) {
// only allow on throw and end events
return (
getParent(parent, 'bpmn:IntermediateThrowEvent') ||
getParent(parent, 'bpmn:EndEvent')
);
}
return true;
};
CamundaModdleExtension.prototype.canHostIn = function (parent) {
const callActivity = getParent(parent, 'bpmn:CallActivity');
if (callActivity) {
return true;
}
const signalEventDefinition = getParent(parent, 'bpmn:SignalEventDefinition');
if (signalEventDefinition) {
// only allow on throw and end events
return (
getParent(parent, 'bpmn:IntermediateThrowEvent') ||
getParent(parent, 'bpmn:EndEvent')
);
}
return true;
};
// module.exports = CamundaModdleExtension;
export default CamundaModdleExtension;
// helpers //////////
function is(element, type) {
return (
element && isFunction(element.$instanceOf) && element.$instanceOf(type)
);
}
function isAny(element, types) {
return some(types, (t) => {
return is(element, t);
});
}
function getParent(element, type) {
if (!type) {
return element.$parent;
}
if (is(element, type)) {
return element;
}
if (!element.$parent) {
return;
}
return getParent(element.$parent, type);
}
function isAllowedInParent(property, parent) {
// (1) find property descriptor
const descriptor =
property.$type && property.$model.getTypeDescriptor(property.$type);
const allowedIn = descriptor && descriptor.meta && descriptor.meta.allowedIn;
if (!allowedIn || isWildcard(allowedIn)) {
return true;
}
// (2) check wether property has parent of allowed type
return some(allowedIn, (type) => {
return getParent(parent, type);
});
}
function isWildcard(allowedIn) {
return allowedIn.includes(WILDCARD);
}

View File

@ -0,0 +1,8 @@
'use strict';
import extension from './extension';
export default {
__init__: ['camundaModdleExtension'],
camundaModdleExtension: ['type', extension],
};

View File

@ -0,0 +1,101 @@
'use strict';
import { some } from 'min-dash';
// const some = some
// const some = require('min-dash').some
const ALLOWED_TYPES = {
FailedJobRetryTimeCycle: [
'bpmn:StartEvent',
'bpmn:BoundaryEvent',
'bpmn:IntermediateCatchEvent',
'bpmn:Activity',
],
Connector: ['bpmn:EndEvent', 'bpmn:IntermediateThrowEvent'],
Field: ['bpmn:EndEvent', 'bpmn:IntermediateThrowEvent'],
};
function is(element, type) {
return (
element &&
typeof element.$instanceOf === 'function' &&
element.$instanceOf(type)
);
}
function exists(element) {
return element && element.length;
}
function includesType(collection, type) {
return (
exists(collection) &&
some(collection, (element) => {
return is(element, type);
})
);
}
function anyType(element, types) {
return some(types, (type) => {
return is(element, type);
});
}
function isAllowed(propName, propDescriptor, newElement) {
const name = propDescriptor.name;
const types = ALLOWED_TYPES[name.replace(/flowable:/, '')];
return name === propName && anyType(newElement, types);
}
function FlowableModdleExtension(eventBus) {
eventBus.on(
'property.clone',
function (context) {
const newElement = context.newElement;
const propDescriptor = context.propertyDescriptor;
this.canCloneProperty(newElement, propDescriptor);
},
this,
);
}
FlowableModdleExtension.$inject = ['eventBus'];
FlowableModdleExtension.prototype.canCloneProperty = function (
newElement,
propDescriptor,
) {
if (
isAllowed('flowable:FailedJobRetryTimeCycle', propDescriptor, newElement)
) {
return (
includesType(newElement.eventDefinitions, 'bpmn:TimerEventDefinition') ||
includesType(newElement.eventDefinitions, 'bpmn:SignalEventDefinition') ||
is(
newElement.loopCharacteristics,
'bpmn:MultiInstanceLoopCharacteristics',
)
);
}
if (isAllowed('flowable:Connector', propDescriptor, newElement)) {
return includesType(
newElement.eventDefinitions,
'bpmn:MessageEventDefinition',
);
}
if (isAllowed('flowable:Field', propDescriptor, newElement)) {
return includesType(
newElement.eventDefinitions,
'bpmn:MessageEventDefinition',
);
}
};
// module.exports = FlowableModdleExtension;
export default FlowableModdleExtension;

View File

@ -0,0 +1,10 @@
/*
* @author igdianov
* address https://github.com/igdianov/activiti-bpmn-moddle
* */
import flowableExtension from './flowableExtension';
export default {
__init__: ['FlowableModdleExtension'],
FlowableModdleExtension: ['type', flowableExtension],
};

View File

@ -0,0 +1,233 @@
import PaletteProvider from 'bpmn-js/lib/features/palette/PaletteProvider';
import { assign } from 'min-dash';
export default function CustomPalette(
palette,
create,
elementFactory,
spaceTool,
lassoTool,
handTool,
globalConnect,
translate,
) {
PaletteProvider.call(
this,
palette,
create,
elementFactory,
spaceTool,
lassoTool,
handTool,
globalConnect,
translate,
2000,
);
}
const F = function () {}; // 核心,利用空对象作为中介;
F.prototype = PaletteProvider.prototype; // 核心将父类的原型赋值给空对象F
// 利用中介函数重写原型链方法
F.prototype.getPaletteEntries = function () {
const actions = {};
const create = this._create;
const elementFactory = this._elementFactory;
const spaceTool = this._spaceTool;
const lassoTool = this._lassoTool;
const handTool = this._handTool;
const globalConnect = this._globalConnect;
const translate = this._translate;
function createAction(type, group, className, title, options) {
function createListener(event) {
const shape = elementFactory.createShape(assign({ type }, options));
if (options) {
shape.businessObject.di.isExpanded = options.isExpanded;
}
create.start(event, shape);
}
const shortType = type.replace(/^bpmn:/, '');
return {
group,
className,
title: title || translate('Create {type}', { type: shortType }),
action: {
dragstart: createListener,
click: createListener,
},
};
}
function createSubprocess(event) {
const subProcess = elementFactory.createShape({
type: 'bpmn:SubProcess',
x: 0,
y: 0,
isExpanded: true,
});
const startEvent = elementFactory.createShape({
type: 'bpmn:StartEvent',
x: 40,
y: 82,
parent: subProcess,
});
create.start(event, [subProcess, startEvent], {
hints: {
autoSelect: [startEvent],
},
});
}
function createParticipant(event) {
create.start(event, elementFactory.createParticipantShape());
}
assign(actions, {
'hand-tool': {
group: 'tools',
className: 'bpmn-icon-hand-tool',
title: '激活抓手工具',
// title: translate("Activate the hand tool"),
action: {
click(event) {
handTool.activateHand(event);
},
},
},
'lasso-tool': {
group: 'tools',
className: 'bpmn-icon-lasso-tool',
title: translate('Activate the lasso tool'),
action: {
click(event) {
lassoTool.activateSelection(event);
},
},
},
'space-tool': {
group: 'tools',
className: 'bpmn-icon-space-tool',
title: translate('Activate the create/remove space tool'),
action: {
click(event) {
spaceTool.activateSelection(event);
},
},
},
'global-connect-tool': {
group: 'tools',
className: 'bpmn-icon-connection-multi',
title: translate('Activate the global connect tool'),
action: {
click(event) {
globalConnect.toggle(event);
},
},
},
'tool-separator': {
group: 'tools',
separator: true,
},
'create.start-event': createAction(
'bpmn:StartEvent',
'event',
'bpmn-icon-start-event-none',
translate('Create StartEvent'),
),
'create.intermediate-event': createAction(
'bpmn:IntermediateThrowEvent',
'event',
'bpmn-icon-intermediate-event-none',
translate('Create Intermediate/Boundary Event'),
),
'create.end-event': createAction(
'bpmn:EndEvent',
'event',
'bpmn-icon-end-event-none',
translate('Create EndEvent'),
),
'create.exclusive-gateway': createAction(
'bpmn:ExclusiveGateway',
'gateway',
'bpmn-icon-gateway-none',
translate('Create Gateway'),
),
'create.user-task': createAction(
'bpmn:UserTask',
'activity',
'bpmn-icon-user-task',
translate('Create User Task'),
),
'create.call-activity': createAction(
'bpmn:CallActivity',
'activity',
'bpmn-icon-call-activity',
translate('Create Call Activity'),
),
'create.service-task': createAction(
'bpmn:ServiceTask',
'activity',
'bpmn-icon-service',
translate('Create Service Task'),
),
'create.data-object': createAction(
'bpmn:DataObjectReference',
'data-object',
'bpmn-icon-data-object',
translate('Create DataObjectReference'),
),
'create.data-store': createAction(
'bpmn:DataStoreReference',
'data-store',
'bpmn-icon-data-store',
translate('Create DataStoreReference'),
),
'create.subprocess-expanded': {
group: 'activity',
className: 'bpmn-icon-subprocess-expanded',
title: translate('Create expanded SubProcess'),
action: {
dragstart: createSubprocess,
click: createSubprocess,
},
},
'create.participant-expanded': {
group: 'collaboration',
className: 'bpmn-icon-participant',
title: translate('Create Pool/Participant'),
action: {
dragstart: createParticipant,
click: createParticipant,
},
},
'create.group': createAction(
'bpmn:Group',
'artifact',
'bpmn-icon-group',
translate('Create Group'),
),
});
return actions;
};
CustomPalette.$inject = [
'palette',
'create',
'elementFactory',
'spaceTool',
'lassoTool',
'handTool',
'globalConnect',
'translate',
];
CustomPalette.prototype = new F(); // 核心,将 F的实例赋值给子类
CustomPalette.prototype.constructor = CustomPalette; // 修复子类CustomPalette的构造器指向防止原型链的混乱

View File

@ -0,0 +1,22 @@
// import PaletteModule from "diagram-js/lib/features/palette";
// import CreateModule from "diagram-js/lib/features/create";
// import SpaceToolModule from "diagram-js/lib/features/space-tool";
// import LassoToolModule from "diagram-js/lib/features/lasso-tool";
// import HandToolModule from "diagram-js/lib/features/hand-tool";
// import GlobalConnectModule from "diagram-js/lib/features/global-connect";
// import translate from "diagram-js/lib/i18n/translate";
//
// import PaletteProvider from "./paletteProvider";
//
// export default {
// __depends__: [PaletteModule, CreateModule, SpaceToolModule, LassoToolModule, HandToolModule, GlobalConnectModule, translate],
// __init__: ["paletteProvider"],
// paletteProvider: ["type", PaletteProvider]
// };
import CustomPalette from './CustomPalette';
export default {
__init__: ['paletteProvider'],
paletteProvider: ['type', CustomPalette],
};

View File

@ -0,0 +1,219 @@
import { assign } from 'min-dash';
/**
* A palette provider for BPMN 2.0 elements.
*/
export default function PaletteProvider(
palette,
create,
elementFactory,
spaceTool,
lassoTool,
handTool,
globalConnect,
translate,
) {
this._palette = palette;
this._create = create;
this._elementFactory = elementFactory;
this._spaceTool = spaceTool;
this._lassoTool = lassoTool;
this._handTool = handTool;
this._globalConnect = globalConnect;
this._translate = translate;
palette.registerProvider(this);
}
PaletteProvider.$inject = [
'palette',
'create',
'elementFactory',
'spaceTool',
'lassoTool',
'handTool',
'globalConnect',
'translate',
];
PaletteProvider.prototype.getPaletteEntries = function () {
const actions = {};
const create = this._create;
const elementFactory = this._elementFactory;
const spaceTool = this._spaceTool;
const lassoTool = this._lassoTool;
const handTool = this._handTool;
const globalConnect = this._globalConnect;
const translate = this._translate;
function createAction(type, group, className, title, options) {
function createListener(event) {
const shape = elementFactory.createShape(assign({ type }, options));
if (options) {
shape.businessObject.di.isExpanded = options.isExpanded;
}
create.start(event, shape);
}
const shortType = type.replace(/^bpmn:/, '');
return {
group,
className,
title: title || translate('Create {type}', { type: shortType }),
action: {
dragstart: createListener,
click: createListener,
},
};
}
function createSubprocess(event) {
const subProcess = elementFactory.createShape({
type: 'bpmn:SubProcess',
x: 0,
y: 0,
isExpanded: true,
});
const startEvent = elementFactory.createShape({
type: 'bpmn:StartEvent',
x: 40,
y: 82,
parent: subProcess,
});
create.start(event, [subProcess, startEvent], {
hints: {
autoSelect: [startEvent],
},
});
}
function createParticipant(event) {
create.start(event, elementFactory.createParticipantShape());
}
assign(actions, {
'hand-tool': {
group: 'tools',
className: 'bpmn-icon-hand-tool',
title: translate('Activate the hand tool'),
action: {
click(event) {
handTool.activateHand(event);
},
},
},
'lasso-tool': {
group: 'tools',
className: 'bpmn-icon-lasso-tool',
title: translate('Activate the lasso tool'),
action: {
click(event) {
lassoTool.activateSelection(event);
},
},
},
'space-tool': {
group: 'tools',
className: 'bpmn-icon-space-tool',
title: translate('Activate the create/remove space tool'),
action: {
click(event) {
spaceTool.activateSelection(event);
},
},
},
'global-connect-tool': {
group: 'tools',
className: 'bpmn-icon-connection-multi',
title: translate('Activate the global connect tool'),
action: {
click(event) {
globalConnect.toggle(event);
},
},
},
'tool-separator': {
group: 'tools',
separator: true,
},
'create.start-event': createAction(
'bpmn:StartEvent',
'event',
'bpmn-icon-start-event-none',
translate('Create StartEvent'),
),
'create.intermediate-event': createAction(
'bpmn:IntermediateThrowEvent',
'event',
'bpmn-icon-intermediate-event-none',
translate('Create Intermediate/Boundary Event'),
),
'create.end-event': createAction(
'bpmn:EndEvent',
'event',
'bpmn-icon-end-event-none',
translate('Create EndEvent'),
),
'create.exclusive-gateway': createAction(
'bpmn:ExclusiveGateway',
'gateway',
'bpmn-icon-gateway-none',
translate('Create Gateway'),
),
'create.user-task': createAction(
'bpmn:UserTask',
'activity',
'bpmn-icon-user-task',
translate('Create User Task'),
),
'create.service-task': createAction(
'bpmn:ServiceTask',
'activity',
'bpmn-icon-service',
translate('Create Service Task'),
),
'create.data-object': createAction(
'bpmn:DataObjectReference',
'data-object',
'bpmn-icon-data-object',
translate('Create DataObjectReference'),
),
'create.data-store': createAction(
'bpmn:DataStoreReference',
'data-store',
'bpmn-icon-data-store',
translate('Create DataStoreReference'),
),
'create.subprocess-expanded': {
group: 'activity',
className: 'bpmn-icon-subprocess-expanded',
title: translate('Create expanded SubProcess'),
action: {
dragstart: createSubprocess,
click: createSubprocess,
},
},
'create.participant-expanded': {
group: 'collaboration',
className: 'bpmn-icon-participant',
title: translate('Create Pool/Participant'),
action: {
dragstart: createParticipant,
click: createParticipant,
},
},
'create.group': createAction(
'bpmn:Group',
'artifact',
'bpmn-icon-group',
translate('Create Group'),
),
});
return actions;
};

View File

@ -0,0 +1,44 @@
// import translations from "./zh";
//
// export default function customTranslate(template, replacements) {
// replacements = replacements || {};
//
// // Translate
// template = translations[template] || template;
//
// // Replace
// return template.replace(/{([^}]+)}/g, function(_, key) {
// let str = replacements[key];
// if (
// translations[replacements[key]] !== null &&
// translations[replacements[key]] !== "undefined"
// ) {
// // eslint-disable-next-line no-mixed-spaces-and-tabs
// str = translations[replacements[key]];
// // eslint-disable-next-line no-mixed-spaces-and-tabs
// }
// return str || "{" + key + "}";
// });
// }
export default function customTranslate(translations) {
return function (template, replacements) {
replacements = replacements || {};
// 将模板和翻译字典的键统一转换为小写进行匹配
const lowerTemplate = template.toLowerCase();
const translation = Object.keys(translations).find(
(key) => key.toLowerCase() === lowerTemplate,
);
// 如果找到匹配的翻译,使用翻译后的模板
if (translation) {
template = translations[translation];
}
// 替换模板中的占位符
return template.replaceAll(/\{([^}]+)\}/g, (_, key) => {
// 如果替换值存在,返回替换值;否则返回原始占位符
return replacements[key] === undefined ? `{${key}}` : replacements[key];
});
};
}

View File

@ -0,0 +1,246 @@
/**
* This is a sample file that should be replaced with the actual translation.
*
* Checkout https://github.com/bpmn-io/bpmn-js-i18n for a list of available
* translations and labels to translate.
*/
export default {
// 添加部分
'Append EndEvent': '追加结束事件',
'Append Gateway': '追加网关',
'Append Task': '追加任务',
'Append Intermediate/Boundary Event': '追加中间抛出事件/边界事件',
'Activate the global connect tool': '激活全局连接工具',
'Append {type}': '添加 {type}',
'Add Lane above': '在上面添加道',
'Divide into two Lanes': '分割成两个道',
'Divide into three Lanes': '分割成三个道',
'Add Lane below': '在下面添加道',
'Append compensation activity': '追加补偿活动',
'Change type': '修改类型',
'Connect using Association': '使用关联连接',
'Connect using Sequence/MessageFlow or Association':
'使用顺序/消息流或者关联连接',
'Connect using DataInputAssociation': '使用数据输入关联连接',
Remove: '移除',
'Activate the hand tool': '激活抓手工具',
'Activate the lasso tool': '激活套索工具',
'Activate the create/remove space tool': '激活创建/删除空间工具',
'Create expanded SubProcess': '创建扩展子过程',
'Create IntermediateThrowEvent/BoundaryEvent': '创建中间抛出事件/边界事件',
'Create Pool/Participant': '创建池/参与者',
'Parallel Multi Instance': '并行多重事件',
'Sequential Multi Instance': '时序多重事件',
DataObjectReference: '数据对象参考',
DataStoreReference: '数据存储参考',
Loop: '循环',
'Ad-hoc': '即席',
'Create {type}': '创建 {type}',
Task: '任务',
'Send Task': '发送任务',
'Receive Task': '接收任务',
'User Task': '用户任务',
'Manual Task': '手工任务',
'Business Rule Task': '业务规则任务',
'Service Task': '服务任务',
'Script Task': '脚本任务',
'Call Activity': '调用活动',
'Sub-Process (collapsed)': '子流程(折叠的)',
'Sub-Process (expanded)': '子流程(展开的)',
'Start Event': '开始事件',
StartEvent: '开始事件',
'Intermediate Throw Event': '中间事件',
'End Event': '结束事件',
EndEvent: '结束事件',
'Create StartEvent': '创建开始事件',
'Create EndEvent': '创建结束事件',
'Create Task': '创建任务',
'Create User Task': '创建用户任务',
'Create Call Activity': '创建调用活动',
'Create Service Task': '创建服务任务',
'Create Gateway': '创建网关',
'Create DataObjectReference': '创建数据对象',
'Create DataStoreReference': '创建数据存储',
'Create Group': '创建分组',
'Create Intermediate/Boundary Event': '创建中间/边界事件',
'Message Start Event': '消息开始事件',
'Timer Start Event': '定时开始事件',
'Conditional Start Event': '条件开始事件',
'Signal Start Event': '信号开始事件',
'Error Start Event': '错误开始事件',
'Escalation Start Event': '升级开始事件',
'Compensation Start Event': '补偿开始事件',
'Message Start Event (non-interrupting)': '消息开始事件(非中断)',
'Timer Start Event (non-interrupting)': '定时开始事件(非中断)',
'Conditional Start Event (non-interrupting)': '条件开始事件(非中断)',
'Signal Start Event (non-interrupting)': '信号开始事件(非中断)',
'Escalation Start Event (non-interrupting)': '升级开始事件(非中断)',
'Message Intermediate Catch Event': '消息中间捕获事件',
'Message Intermediate Throw Event': '消息中间抛出事件',
'Timer Intermediate Catch Event': '定时中间捕获事件',
'Escalation Intermediate Throw Event': '升级中间抛出事件',
'Conditional Intermediate Catch Event': '条件中间捕获事件',
'Link Intermediate Catch Event': '链接中间捕获事件',
'Link Intermediate Throw Event': '链接中间抛出事件',
'Compensation Intermediate Throw Event': '补偿中间抛出事件',
'Signal Intermediate Catch Event': '信号中间捕获事件',
'Signal Intermediate Throw Event': '信号中间抛出事件',
'Message End Event': '消息结束事件',
'Escalation End Event': '定时结束事件',
'Error End Event': '错误结束事件',
'Cancel End Event': '取消结束事件',
'Compensation End Event': '补偿结束事件',
'Signal End Event': '信号结束事件',
'Terminate End Event': '终止结束事件',
'Message Boundary Event': '消息边界事件',
'Message Boundary Event (non-interrupting)': '消息边界事件(非中断)',
'Timer Boundary Event': '定时边界事件',
'Timer Boundary Event (non-interrupting)': '定时边界事件(非中断)',
'Escalation Boundary Event': '升级边界事件',
'Escalation Boundary Event (non-interrupting)': '升级边界事件(非中断)',
'Conditional Boundary Event': '条件边界事件',
'Conditional Boundary Event (non-interrupting)': '条件边界事件(非中断)',
'Error Boundary Event': '错误边界事件',
'Cancel Boundary Event': '取消边界事件',
'Signal Boundary Event': '信号边界事件',
'Signal Boundary Event (non-interrupting)': '信号边界事件(非中断)',
'Compensation Boundary Event': '补偿边界事件',
'Exclusive Gateway': '互斥网关',
'Parallel Gateway': '并行网关',
'Inclusive Gateway': '相容网关',
'Complex Gateway': '复杂网关',
'Event based Gateway': '事件网关',
Transaction: '转运',
'Sub Process': '子流程',
'Event Sub Process': '事件子流程',
'Collapsed Pool': '折叠池',
'Expanded Pool': '展开池',
// Errors
'no parent for {element} in {parent}': '在{parent}里,{element}没有父类',
'no shape type specified': '没有指定的形状类型',
'flow elements must be children of pools/participants':
'流元素必须是池/参与者的子类',
'out of bounds release': 'out of bounds release',
'more than {count} child lanes': '子道大于{count} ',
'element required': '元素不能为空',
'diagram not part of bpmn:Definitions': '流程图不符合bpmn规范',
'no diagram to display': '没有可展示的流程图',
'no process or collaboration to display': '没有可展示的流程/协作',
'element {element} referenced by {referenced}#{property} not yet drawn':
'由{referenced}#{property}引用的{element}元素仍未绘制',
'already rendered {element}': '{element} 已被渲染',
'failed to import {element}': '导入{element}失败',
// 属性面板的参数
Id: '编号',
Name: '名称',
General: '常规',
Details: '详情',
'Message Name': '消息名称',
Message: '消息',
Initiator: '创建者',
'Asynchronous Continuations': '持续异步',
'Asynchronous Before': '异步前',
'Asynchronous After': '异步后',
'Job Configuration': '工作配置',
Exclusive: '排除',
'Job Priority': '工作优先级',
'Retry Time Cycle': '重试时间周期',
Documentation: '文档',
'Element Documentation': '元素文档',
'History Configuration': '历史配置',
'History Time To Live': '历史的生存时间',
Forms: '表单',
'Form Key': '表单key',
'Form Fields': '表单字段',
'Business Key': '业务key',
'Form Field': '表单字段',
ID: '编号',
Type: '类型',
Label: '名称',
'Default Value': '默认值',
'Default Flow': '默认流转路径',
'Conditional Flow': '条件流转路径',
'Sequence Flow': '普通流转路径',
Validation: '校验',
'Add Constraint': '添加约束',
Config: '配置',
Properties: '属性',
'Add Property': '添加属性',
Value: '值',
Listeners: '监听器',
'Execution Listener': '执行监听',
'Event Type': '事件类型',
'Listener Type': '监听器类型',
'Java Class': 'Java类',
Expression: '表达式',
'Must provide a value': '必须提供一个值',
'Delegate Expression': '代理表达式',
Script: '脚本',
'Script Format': '脚本格式',
'Script Type': '脚本类型',
'Inline Script': '内联脚本',
'External Script': '外部脚本',
Resource: '资源',
'Field Injection': '字段注入',
Extensions: '扩展',
'Input/Output': '输入/输出',
'Input Parameters': '输入参数',
'Output Parameters': '输出参数',
Parameters: '参数',
'Output Parameter': '输出参数',
'Timer Definition Type': '定时器定义类型',
'Timer Definition': '定时器定义',
Date: '日期',
Duration: '持续',
Cycle: '循环',
Signal: '信号',
'Signal Name': '信号名称',
Escalation: '升级',
Error: '错误',
'Link Name': '链接名称',
Condition: '条件名称',
'Variable Name': '变量名称',
'Variable Event': '变量事件',
'Specify more than one variable change event as a comma separated list.':
'多个变量事件以逗号隔开',
'Wait for Completion': '等待完成',
'Activity Ref': '活动参考',
'Version Tag': '版本标签',
Executable: '可执行文件',
'External Task Configuration': '扩展任务配置',
'Task Priority': '任务优先级',
External: '外部',
Connector: '连接器',
'Must configure Connector': '必须配置连接器',
'Connector Id': '连接器编号',
Implementation: '实现方式',
'Field Injections': '字段注入',
Fields: '字段',
'Result Variable': '结果变量',
Topic: '主题',
'Configure Connector': '配置连接器',
'Input Parameter': '输入参数',
Assignee: '代理人',
'Candidate Users': '候选用户',
'Candidate Groups': '候选组',
'Due Date': '到期时间',
'Follow Up Date': '跟踪日期',
Priority: '优先级',
'The follow up date as an EL expression (e.g. ${someDate} or an ISO date (e.g. 2015-06-26T09:54:00)':
'跟踪日期必须符合EL表达式 ${someDate} ,或者一个ISO标准日期2015-06-26T09:54:00',
'The due date as an EL expression (e.g. ${someDate} or an ISO date (e.g. 2015-06-26T09:54:00)':
'跟踪日期必须符合EL表达式 ${someDate} ,或者一个ISO标准日期2015-06-26T09:54:00',
Variables: '变量',
'Candidate Starter Configuration': '候选人起动器配置',
'Candidate Starter Groups': '候选人起动器组',
'This maps to the process definition key.': '这映射到流程定义键。',
'Candidate Starter Users': '候选人起动器的用户',
'Specify more than one user as a comma separated list.':
'指定多个用户作为逗号分隔的列表。',
'Tasklist Configuration': 'Tasklist配置',
Startable: '启动',
'Specify more than one group as a comma separated list.':
'指定多个组作为逗号分隔的列表。',
};

View File

@ -0,0 +1,9 @@
import './theme/index.scss';
import 'bpmn-js/dist/assets/diagram-js.css';
import 'bpmn-js/dist/assets/bpmn-font/css/bpmn.css';
import 'bpmn-js/dist/assets/bpmn-font/css/bpmn-codes.css';
import 'bpmn-js/dist/assets/bpmn-font/css/bpmn-embedded.css';
export { default as MyProcessDesigner } from './designer';
export { default as MyProcessViewer } from './designer/index2';
export { default as MyProcessPenal } from './penal';

View File

@ -0,0 +1,37 @@
<script lang="ts" setup>
import { Button } from 'ant-design-vue';
import { assign } from 'min-dash';
defineOptions({ name: 'MyProcessPalette' });
const bpmnInstances = () =>
(window as typeof window & { bpmnInstances?: any }).bpmnInstances;
const addTask = (event: MouseEvent, options: any = {}) => {
const ElementFactory = bpmnInstances().elementFactory;
const create = bpmnInstances().modeler.get('create');
const shape = ElementFactory.createShape(
assign({ type: 'bpmn:UserTask' }, options),
);
if (options) {
shape.businessObject.di.isExpanded = options.isExpanded;
}
create.start(event, shape);
};
</script>
<template>
<div class="my-process-palette p-20 pt-80">
<Button type="primary" @click="addTask" @mousedown="addTask">
测试任务
</Button>
<div class="test-container" id="palette-container">1</div>
</div>
</template>
<style scoped>
.test-container {
margin-top: 16px;
}
</style>

View File

@ -0,0 +1,400 @@
<script lang="ts" setup>
import { nextTick, onBeforeUnmount, onMounted, provide, ref, watch } from 'vue';
import { IconifyIcon } from '@vben/icons';
import { cloneDeep } from '@vben/utils';
import { Collapse } from 'ant-design-vue';
import ElementCustomConfig from '#/components/bpmn-process-designer/package/penal/custom-config/ElementCustomConfig.vue';
import ElementForm from '#/components/bpmn-process-designer/package/penal/form/ElementForm.vue';
import ElementBaseInfo from './base/ElementBaseInfo.vue';
import FlowCondition from './flow-condition/FlowCondition.vue';
import ElementListeners from './listeners/ElementListeners.vue';
// import ElementForm from './form/ElementForm.vue'
import UserTaskListeners from './listeners/UserTaskListeners.vue';
import ElementMultiInstance from './multi-instance/ElementMultiInstance.vue';
import ElementOtherConfig from './other/ElementOtherConfig.vue';
import ElementProperties from './properties/ElementProperties.vue';
import SignalAndMassage from './signal-message/SignalAndMessage.vue';
import { getTaskCollapseItemName, isTaskCollapseItemShow } from './task/data';
import ElementTask from './task/ElementTask.vue';
import TimeEventConfig from './time-event-config/TimeEventConfig.vue';
defineOptions({ name: 'MyPropertiesPanel' });
/**
* 侧边栏
* @Author MiyueFE
* @Home https://github.com/miyuesc
* @Date 2021年3月31日18:57:51
*/
const props = defineProps({
bpmnModeler: {
type: Object,
default: () => ({}),
},
prefix: {
type: String,
default: 'camunda',
},
width: {
type: Number,
default: 480,
},
idEditDisabled: {
type: Boolean,
default: false,
},
businessObject: {
type: Object,
default: () => ({}),
},
model: {
type: Object,
default: () => ({}),
}, //
});
const CollapsePanel = Collapse.Panel;
const activeTab = ref('base');
const elementId = ref('');
const elementType = ref<any>('');
const elementBusinessObject = ref<any>({}); // businessObject 使
const conditionFormVisible = ref(false); //
const formVisible = ref(false); //
const bpmnElement = ref();
const isReady = ref(false);
const type = ref('time');
const condition = ref('');
provide('prefix', props.prefix);
provide('width', props.width);
// bpmnInstances
const initBpmnInstances = () => {
if (!props.bpmnModeler) return false;
try {
const instances = {
modeler: props.bpmnModeler,
modeling: props.bpmnModeler.get('modeling'),
moddle: props.bpmnModeler.get('moddle'),
eventBus: props.bpmnModeler.get('eventBus'),
bpmnFactory: props.bpmnModeler.get('bpmnFactory'),
elementFactory: props.bpmnModeler.get('elementFactory'),
elementRegistry: props.bpmnModeler.get('elementRegistry'),
replace: props.bpmnModeler.get('replace'),
selection: props.bpmnModeler.get('selection'),
};
//
const allInstancesExist = Object.values(instances).every(Boolean);
if (allInstancesExist) {
const w = window as any;
w.bpmnInstances = instances;
return true;
}
return false;
} catch (error) {
console.error('初始化 bpmnInstances 失败:', error);
return false;
}
};
const bpmnInstances = () => (window as any)?.bpmnInstances;
// props.bpmnModeler initModels
watch(
() => props.bpmnModeler,
async () => {
//
if (!props.bpmnModeler) {
// console.log('props.bpmnModeler');
return;
}
try {
// modeler
await nextTick();
if (initBpmnInstances()) {
isReady.value = true;
await nextTick();
getActiveElement();
} else {
console.error('modeler 实例未完全初始化');
}
} catch (error) {
console.error('初始化失败:', error);
}
},
{
immediate: true,
},
);
const getActiveElement = () => {
if (!isReady.value || !props.bpmnModeler) return;
// bpmn:Process
initFormOnChanged(null);
props.bpmnModeler.on('import.done', (_: any) => {
// console.log(e, 'eeeee');
initFormOnChanged(null);
});
//
props.bpmnModeler.on(
'selection.changed',
({ newSelection }: { newSelection: any }) => {
initFormOnChanged(newSelection[0] || null);
},
);
props.bpmnModeler.on('element.changed', ({ element }: { element: any }) => {
// ""
if (element && element.id === elementId.value) {
initFormOnChanged(element);
}
});
};
//
const initFormOnChanged = (element: any) => {
if (!isReady.value || !bpmnInstances()) return;
let activatedElement = element;
if (!activatedElement) {
activatedElement =
bpmnInstances().elementRegistry.find(
(el: any) => el.type === 'bpmn:Process',
) ??
bpmnInstances().elementRegistry.find(
(el: any) => el.type === 'bpmn:Collaboration',
);
}
if (!activatedElement) return;
try {
// console.log(`
// ----------
// select element changed:
// id: ${activatedElement.id}
// type: ${activatedElement.businessObject.$type}
// ----------
// `);
// console.log('businessObject:', activatedElement.businessObject);
bpmnInstances().bpmnElement = activatedElement;
bpmnElement.value = activatedElement;
elementId.value = activatedElement.id;
elementType.value = activatedElement.type.split(':')[1] || '';
elementBusinessObject.value = cloneDeep(activatedElement.businessObject);
conditionFormVisible.value =
elementType.value === 'SequenceFlow' &&
activatedElement.source &&
(activatedElement.source.type as string).includes('StartEvent');
formVisible.value =
elementType.value === 'UserTask' || elementType.value === 'StartEvent';
} catch (error) {
console.error('初始化表单数据失败:', error);
}
};
onBeforeUnmount(() => {
const w = window as any;
w.bpmnInstances = null;
isReady.value = false;
});
watch(
() => elementId.value,
() => {
activeTab.value = 'base';
},
);
//
// function updateNode() {
// const moddle = window.bpmnInstances?.moddle;
// const modeling = window.bpmnInstances?.modeling;
// const elementRegistry = window.bpmnInstances?.elementRegistry;
// if (!moddle || !modeling || !elementRegistry) return;
//
// const element = elementRegistry.get(props.businessObject.id);
// if (!element) return;
//
// const timerDef = moddle.create('bpmn:TimerEventDefinition', {});
// switch (type.value) {
// case 'cycle': {
// timerDef.timeCycle = moddle.create('bpmn:FormalExpression', {
// body: condition.value,
// });
//
// break;
// }
// case 'duration': {
// timerDef.timeDuration = moddle.create('bpmn:FormalExpression', {
// body: condition.value,
// });
//
// break;
// }
// case 'time': {
// timerDef.timeDate = moddle.create('bpmn:FormalExpression', {
// body: condition.value,
// });
//
// break;
// }
// // No default
// }
//
// modeling.updateModdleProperties(element, element.businessObject, {
// eventDefinitions: [timerDef],
// });
// }
//
function syncFromBusinessObject() {
if (props.businessObject) {
const timerDef = (props.businessObject.eventDefinitions || [])[0];
if (timerDef) {
if (timerDef.timeDate) {
type.value = 'time';
condition.value = timerDef.timeDate.body;
} else if (timerDef.timeDuration) {
type.value = 'duration';
condition.value = timerDef.timeDuration.body;
} else if (timerDef.timeCycle) {
type.value = 'cycle';
condition.value = timerDef.timeCycle.body;
}
}
}
}
onMounted(syncFromBusinessObject);
watch(() => props.businessObject, syncFromBusinessObject, { deep: true });
</script>
<template>
<div
class="process-panel__container"
:style="{ width: `${width}px`, maxHeight: '600px' }"
>
<Collapse v-model:active-key="activeTab" v-if="isReady">
<CollapsePanel key="base" header="常规">
<template #extra>
<IconifyIcon icon="ep:info-filled" />
</template>
<ElementBaseInfo
:id-edit-disabled="idEditDisabled"
:business-object="elementBusinessObject"
:type="elementType"
:model="model"
/>
</CollapsePanel>
<CollapsePanel
key="message"
header="消息与信号"
v-if="elementType === 'Process'"
>
<template #extra>
<IconifyIcon icon="ep:comment" />
</template>
<SignalAndMassage />
</CollapsePanel>
<CollapsePanel
key="condition"
header="流转条件"
v-if="conditionFormVisible"
>
<template #extra>
<IconifyIcon icon="ep:promotion" />
</template>
<FlowCondition
:business-object="elementBusinessObject"
:type="elementType"
/>
</CollapsePanel>
<CollapsePanel key="form" header="表单" v-if="formVisible">
<template #extra>
<IconifyIcon icon="ep:list" />
</template>
<ElementForm :id="elementId" :type="elementType" />
</CollapsePanel>
<CollapsePanel
key="task"
:header="getTaskCollapseItemName(elementType)"
v-if="isTaskCollapseItemShow(elementType)"
>
<template #extra>
<IconifyIcon icon="ep:checked" />
</template>
<ElementTask :id="elementId" :type="elementType" />
</CollapsePanel>
<CollapsePanel
key="multiInstance"
header="多人审批方式"
v-if="elementType.includes('Task')"
>
<template #extra>
<IconifyIcon icon="ep:help-filled" />
</template>
<ElementMultiInstance
:id="elementId"
:business-object="elementBusinessObject"
:type="elementType"
/>
</CollapsePanel>
<CollapsePanel key="listeners" header="执行监听器">
<template #extra>
<IconifyIcon icon="ep:bell-filled" />
</template>
<ElementListeners :id="elementId" :type="elementType" />
</CollapsePanel>
<CollapsePanel
key="taskListeners"
header="任务监听器"
v-if="elementType === 'UserTask'"
>
<template #extra>
<IconifyIcon icon="ep:bell-filled" />
</template>
<UserTaskListeners :id="elementId" :type="elementType" />
</CollapsePanel>
<CollapsePanel key="extensions" header="扩展属性">
<template #extra>
<IconifyIcon icon="ep:circle-plus-filled" />
</template>
<ElementProperties :id="elementId" :type="elementType" />
</CollapsePanel>
<CollapsePanel key="other" header="其他">
<template #extra>
<IconifyIcon icon="ep:promotion" />
</template>
<ElementOtherConfig :id="elementId" />
</CollapsePanel>
<CollapsePanel key="customConfig" header="自定义配置">
<template #extra>
<IconifyIcon icon="ep:tools" />
</template>
<ElementCustomConfig
:id="elementId"
:type="elementType"
:business-object="elementBusinessObject"
/>
</CollapsePanel>
<!-- 新增的时间事件配置项 -->
<CollapsePanel
key="timeEvent"
header="时间事件"
v-if="elementType === 'IntermediateCatchEvent'"
>
<template #extra>
<IconifyIcon icon="ep:timer" />
</template>
<TimeEventConfig
:business-object="bpmnElement.value?.businessObject"
:key="elementId"
/>
</CollapsePanel>
</Collapse>
</div>
</template>

View File

@ -0,0 +1,225 @@
<script lang="ts" setup>
import { onBeforeUnmount, reactive, ref, toRaw, watch } from 'vue';
import { Form, FormItem, Input } from 'ant-design-vue';
defineOptions({ name: 'ElementBaseInfo' });
const props = defineProps<{
businessObject?: BusinessObject;
model?: Model;
}>();
interface BusinessObject {
id?: string;
name?: string;
$type: string;
[key: string]: any;
}
interface Model {
key?: string;
name?: string;
[key: string]: any;
}
const needProps = ref<Record<string, any>>({});
const bpmnElement = ref<any>();
const elementBaseInfo = ref<BusinessObject>({} as any);
//
// const forms = ref([])
//
const rules = reactive<any>({
id: [{ required: true, message: '流程标识不能为空', trigger: 'blur' }],
name: [{ required: true, message: '流程名称不能为空', trigger: 'blur' }],
});
const bpmnInstances = () =>
(window as any)?.bpmnInstances as {
bpmnElement: any;
modeling: {
updateProperties: (element: any, properties: any) => void;
};
};
const resetBaseInfo = () => {
// console.log(window, 'window');
// console.log(bpmnElement.value, 'bpmnElement');
bpmnElement.value = bpmnInstances()?.bpmnElement;
// console.log(bpmnElement.value, 'resetBaseInfo11111111111')
if (bpmnElement.value?.businessObject) {
elementBaseInfo.value = bpmnElement.value.businessObject;
needProps.value.type = bpmnElement.value.businessObject.$type;
}
// elementBaseInfo.value['typess'] = bpmnElement.value.businessObject.$type
// elementBaseInfo.value = JSON.parse(JSON.stringify(bpmnElement.value.businessObject))
// console.log(elementBaseInfo.value, 'elementBaseInfo22222222222')
};
const handleKeyUpdate = (value: any) => {
// value XML NCName
if (!value) {
return;
}
if (!/[a-z_][-\w.$]*/i.test(value)) {
// console.log('key XML NCName ');
return;
}
// console.log('key XML NCName ');
// BPMN XML key id
if (elementBaseInfo.value) {
elementBaseInfo.value.id = value;
}
setTimeout(() => {
updateBaseInfo('id');
}, 100);
};
const handleNameUpdate = (value: any) => {
// console.log(elementBaseInfo, 'elementBaseInfo');
if (!value) {
return;
}
if (elementBaseInfo.value) {
elementBaseInfo.value.name = value;
}
setTimeout(() => {
updateBaseInfo('name');
}, 100);
};
// const handleDescriptionUpdate=(value)=> {
// TODO documentation
// this.elementBaseInfo['documentation'] = value;
// this.updateBaseInfo('documentation');
// }
const updateBaseInfo = (key: string) => {
// console.log(key, 'key');
// elementBaseInfo
const attrObj: Record<string, any> = Object.create(null);
//
if (!elementBaseInfo.value || !bpmnElement.value) {
return;
}
// console.log(attrObj, 'attrObj')
attrObj[key] = elementBaseInfo.value[key];
// console.log(attrObj, 'attrObj111')
// const attrObj = {
// id: elementBaseInfo.value[key]
// // di: { id: `${elementBaseInfo.value[key]}_di` }
// }
// console.log(elementBaseInfo, 'elementBaseInfo11111111111')
needProps.value = { ...elementBaseInfo.value, ...needProps.value };
if (key === 'id') {
// console.log('jinru')
// console.log(window, 'window');
// console.log(bpmnElement.value, 'bpmnElement');
// console.log(toRaw(bpmnElement.value), 'bpmnElement');
bpmnInstances().modeling.updateProperties(toRaw(bpmnElement.value), {
id: elementBaseInfo.value[key],
di: { id: `${elementBaseInfo.value[key]}_di` },
});
} else {
// console.log(attrObj, 'attrObj');
bpmnInstances().modeling.updateProperties(
toRaw(bpmnElement.value),
attrObj,
);
}
};
watch(
() => props.businessObject,
(val) => {
// console.log(val, 'val11111111111111111111')
if (val) {
// nextTick(() => {
resetBaseInfo();
// })
}
},
);
watch(
() => props.model?.key,
(val) => {
// bpmn key name
if (val) {
handleKeyUpdate(props.model?.key as any);
handleNameUpdate(props.model?.name as any);
}
},
{
immediate: true,
},
);
// watch(
// () => ({ ...props }),
// (oldVal, newVal) => {
// console.log(oldVal, 'oldVal')
// console.log(newVal, 'newVal')
// if (newVal) {
// needProps.value = newVal
// }
// },
// {
// immediate: true
// }
// )
// 'model.key': {
// immediate: false,
// handler: function (val) {
// this.handleKeyUpdate(val)
// }
// }
onBeforeUnmount(() => {
bpmnElement.value = null;
});
</script>
<template>
<div class="panel-tab__content">
<Form :model="needProps" :rules="rules" layout="vertical">
<div v-if="needProps.type === 'bpmn:Process'">
<!-- 如果是 Process 信息的时候使用自定义表单 -->
<FormItem label="流程标识" name="id">
<Input
v-model:value="needProps.id"
placeholder="请输入流标标识"
:disabled="needProps.id !== undefined && needProps.id.length > 0"
@change="handleKeyUpdate"
/>
</FormItem>
<FormItem label="流程名称" name="name">
<Input
v-model:value="needProps.name"
placeholder="请输入流程名称"
allow-clear
@change="handleNameUpdate"
/>
</FormItem>
</div>
<div v-else>
<FormItem label="ID">
<Input
v-model:value="elementBaseInfo.id"
allow-clear
@change="updateBaseInfo('id')"
/>
</FormItem>
<FormItem label="名称">
<Input
v-model:value="elementBaseInfo.name"
allow-clear
@change="updateBaseInfo('name')"
/>
</FormItem>
</div>
</Form>
</div>
</template>

View File

@ -0,0 +1,58 @@
<script lang="ts" setup>
import type { Component } from 'vue';
import { defineOptions, defineProps, ref, watch } from 'vue';
import { CustomConfigMap } from './data';
defineOptions({ name: 'ElementCustomConfig' });
const props = defineProps({
id: {
type: String,
default: '',
},
type: {
type: String,
default: '',
},
businessObject: {
type: Object as () => BusinessObject,
default: () => ({}),
},
});
interface BusinessObject {
eventDefinitions?: Array<{ $type: string }>;
[key: string]: any;
}
// const bpmnInstances = () => (window as any)?.bpmnInstances;
const customConfigComponent = ref<Component | null>(null);
watch(
() => props.businessObject,
() => {
if (props.type && props.businessObject) {
let val = props.type;
if (props.businessObject.eventDefinitions) {
val +=
props.businessObject.eventDefinitions[0]?.$type.split(':')[1] || '';
}
// @ts-ignore
customConfigComponent.value = (
CustomConfigMap as Record<string, { component: Component }>
)[val]?.component;
}
},
{ immediate: true },
);
</script>
<template>
<div class="panel-tab__content">
<component :is="customConfigComponent" v-bind="$props" />
</div>
</template>
<style lang="scss" scoped></style>

View File

@ -0,0 +1,307 @@
<script lang="ts" setup>
import {
defineOptions,
defineProps,
inject,
nextTick,
ref,
toRaw,
watch,
} from 'vue';
import {
Divider,
FormItem,
InputNumber,
RadioButton,
RadioGroup,
Select,
SelectOption,
Switch,
} from 'ant-design-vue';
import { convertTimeUnit } from '#/components/simple-process-design/components/nodes-config/utils';
import {
TIME_UNIT_TYPES,
TIMEOUT_HANDLER_TYPES,
TimeUnitType,
} from '#/components/simple-process-design/consts';
defineOptions({ name: 'ElementCustomConfig4BoundaryEventTimer' });
const props = defineProps({
id: {
type: String,
default: '',
},
type: {
type: String,
default: '',
},
});
const prefix = inject('prefix');
const bpmnElement = ref<any>();
const bpmnInstances = () => (window as Record<string, any>)?.bpmnInstances;
const timeoutHandlerEnable = ref(false);
const boundaryEventType = ref<any>();
const timeoutHandlerType = ref<{
value: number | undefined;
}>({
value: undefined,
});
const timeModdle = ref<any>();
const timeDuration = ref(6);
const timeUnit = ref(TimeUnitType.HOUR);
const maxRemindCount = ref(1);
const elExtensionElements = ref<any>();
const otherExtensions = ref<any[]>();
const configExtensions = ref<any[]>([]);
const eventDefinition = ref<any>();
const resetElement = () => {
bpmnElement.value = bpmnInstances().bpmnElement;
eventDefinition.value = bpmnElement.value.businessObject.eventDefinitions[0];
//
elExtensionElements.value =
bpmnElement.value.businessObject?.extensionElements ??
bpmnInstances().moddle.create('bpmn:ExtensionElements', { values: [] });
//
boundaryEventType.value = elExtensionElements.value.values?.filter(
(ex: any) => ex.$type === `${prefix}:BoundaryEventType`,
)?.[0];
if (boundaryEventType.value && boundaryEventType.value.value === 1) {
timeoutHandlerEnable.value = true;
configExtensions.value.push(boundaryEventType.value);
}
//
timeoutHandlerType.value = elExtensionElements.value.values?.filter(
(ex: any) => ex.$type === `${prefix}:TimeoutHandlerType`,
)?.[0];
if (timeoutHandlerType.value) {
configExtensions.value.push(timeoutHandlerType.value);
if (eventDefinition.value.timeCycle) {
const timeStr = eventDefinition.value.timeCycle.body;
const maxRemindCountStr = timeStr.split('/')[0];
const timeDurationStr = timeStr.split('/')[1];
maxRemindCount.value = Number.parseInt(maxRemindCountStr.slice(1));
timeDuration.value = Number.parseInt(timeDurationStr.slice(2, -1));
timeUnit.value = convertTimeUnit(timeDurationStr.slice(-1));
timeModdle.value = eventDefinition.value.timeCycle;
}
if (eventDefinition.value.timeDuration) {
const timeDurationStr = eventDefinition.value.timeDuration.body;
timeDuration.value = Number.parseInt(timeDurationStr.slice(2, -1));
timeUnit.value = convertTimeUnit(timeDurationStr.slice(-1));
timeModdle.value = eventDefinition.value.timeDuration;
}
}
// 便
otherExtensions.value =
elExtensionElements.value.values?.filter(
(ex: any) =>
ex.$type !== `${prefix}:BoundaryEventType` &&
ex.$type !== `${prefix}:TimeoutHandlerType`,
) ?? [];
};
const timeoutHandlerChange = (checked: any) => {
timeoutHandlerEnable.value = checked;
if (checked) {
//
// ---
boundaryEventType.value = bpmnInstances().moddle.create(
`${prefix}:BoundaryEventType`,
{
value: 1,
},
);
configExtensions.value.push(boundaryEventType.value);
//
timeoutHandlerType.value = bpmnInstances().moddle.create(
`${prefix}:TimeoutHandlerType`,
{
value: 1,
},
);
configExtensions.value.push(timeoutHandlerType.value);
//
timeDuration.value = 6;
timeUnit.value = 2;
maxRemindCount.value = 1;
timeModdle.value = bpmnInstances().moddle.create(`bpmn:Expression`, {
body: 'PT6H',
});
eventDefinition.value.timeDuration = timeModdle.value;
} else {
//
configExtensions.value = [];
delete eventDefinition.value.timeDuration;
delete eventDefinition.value.timeCycle;
}
updateElementExtensions();
};
const onTimeoutHandlerTypeChanged = () => {
maxRemindCount.value = 1;
updateElementExtensions();
updateTimeModdle();
};
const onTimeUnitChange = () => {
// 60
if (timeUnit.value === TimeUnitType.MINUTE) {
timeDuration.value = 60;
}
// 6
if (timeUnit.value === TimeUnitType.HOUR) {
timeDuration.value = 6;
}
// 1
if (timeUnit.value === TimeUnitType.DAY) {
timeDuration.value = 1;
}
updateTimeModdle();
updateElementExtensions();
};
const updateTimeModdle = () => {
if (maxRemindCount.value > 1) {
timeModdle.value.body = `R${maxRemindCount.value}/${isoTimeDuration()}`;
if (!eventDefinition.value.timeCycle) {
delete eventDefinition.value.timeDuration;
eventDefinition.value.timeCycle = timeModdle.value;
}
} else {
timeModdle.value.body = isoTimeDuration();
if (!eventDefinition.value.timeDuration) {
delete eventDefinition.value.timeCycle;
eventDefinition.value.timeDuration = timeModdle.value;
}
}
};
const isoTimeDuration = () => {
let strTimeDuration = 'PT';
if (timeUnit.value === TimeUnitType.MINUTE) {
strTimeDuration += `${timeDuration.value}M`;
}
if (timeUnit.value === TimeUnitType.HOUR) {
strTimeDuration += `${timeDuration.value}H`;
}
if (timeUnit.value === TimeUnitType.DAY) {
strTimeDuration += `${timeDuration.value}D`;
}
return strTimeDuration;
};
const updateElementExtensions = () => {
const extensions = bpmnInstances().moddle.create('bpmn:ExtensionElements', {
values: [...(otherExtensions.value || []), ...configExtensions.value],
});
bpmnInstances().modeling.updateProperties(toRaw(bpmnElement.value), {
extensionElements: extensions,
});
};
watch(
() => props.id,
(val) => {
val &&
val.length > 0 &&
nextTick(() => {
resetElement();
});
},
{ immediate: true },
);
</script>
<template>
<div>
<Divider orientation="left">审批人超时未处理时</Divider>
<FormItem label="启用开关" name="timeoutHandlerEnable">
<Switch
v-model:checked="timeoutHandlerEnable"
checked-children="开启"
un-checked-children="关闭"
@change="timeoutHandlerChange"
/>
</FormItem>
<FormItem
label="执行动作"
name="timeoutHandlerType"
v-if="timeoutHandlerEnable"
>
<RadioGroup
v-model:value="timeoutHandlerType.value"
@change="onTimeoutHandlerTypeChanged"
>
<RadioButton
v-for="item in TIMEOUT_HANDLER_TYPES"
:key="item.value"
:value="item.value"
>
{{ item.label }}
</RadioButton>
</RadioGroup>
</FormItem>
<FormItem label="超时时间设置" v-if="timeoutHandlerEnable">
<span class="mr-2">当超过</span>
<FormItem name="timeDuration">
<InputNumber
class="mr-2"
:style="{ width: '100px' }"
v-model:value="timeDuration"
:min="1"
:controls="true"
@change="
() => {
updateTimeModdle();
updateElementExtensions();
}
"
/>
</FormItem>
<Select
v-model:value="timeUnit"
class="mr-2"
:style="{ width: '100px' }"
@change="onTimeUnitChange"
>
<SelectOption
v-for="item in TIME_UNIT_TYPES"
:key="item.value"
:value="item.value"
>
{{ item.label }}
</SelectOption>
</Select>
未处理
</FormItem>
<FormItem
label="最大提醒次数"
name="maxRemindCount"
v-if="timeoutHandlerEnable && timeoutHandlerType.value === 1"
>
<InputNumber
v-model:value="maxRemindCount"
:min="1"
:max="10"
@change="
() => {
updateTimeModdle();
updateElementExtensions();
}
"
/>
</FormItem>
</div>
</template>
<style lang="scss" scoped></style>

View File

@ -0,0 +1,783 @@
<!-- UserTask 自定义配置
1. 审批人与提交人为同一人时
2. 审批人拒绝时
3. 审批人为空时
4. 操作按钮
5. 字段权限
6. 审批类型
7. 是否需要签名
-->
<script lang="ts" setup>
import type { SystemUserApi } from '#/api/system/user';
import type { ButtonSetting } from '#/components/simple-process-design/consts';
import { inject, nextTick, onMounted, ref, toRaw, watch } from 'vue';
import { BpmModelFormType } from '@vben/constants';
import {
Button,
Divider,
Form,
Radio,
RadioGroup,
Select,
SelectOption,
Switch,
} from 'ant-design-vue';
import { getSimpleUserList } from '#/api/system/user';
import {
APPROVE_TYPE,
ApproveType,
ASSIGN_EMPTY_HANDLER_TYPES,
ASSIGN_START_USER_HANDLER_TYPES,
AssignEmptyHandlerType,
DEFAULT_BUTTON_SETTING,
FieldPermissionType,
OPERATION_BUTTON_NAME,
REJECT_HANDLER_TYPES,
RejectHandlerType,
} from '#/components/simple-process-design/consts';
import { useFormFieldsPermission } from '#/components/simple-process-design/helpers';
defineOptions({ name: 'ElementCustomConfig4UserTask' });
const props = defineProps({
id: {
type: String,
required: false,
default: '',
},
type: {
type: String,
required: false,
default: '',
},
});
const prefix = inject('prefix');
//
const assignStartUserHandlerTypeEl = ref<any>();
const assignStartUserHandlerType = ref<any>();
//
const rejectHandlerTypeEl = ref<any>();
const rejectHandlerType = ref<any>();
const returnNodeIdEl = ref<any>();
const returnNodeId = ref<any>();
const returnTaskList = ref<any[]>([]);
//
const assignEmptyHandlerTypeEl = ref<any>();
const assignEmptyHandlerType = ref<any>();
const assignEmptyUserIdsEl = ref<any>();
const assignEmptyUserIds = ref<any>();
//
const buttonsSettingEl = ref<any>();
const { btnDisplayNameEdit, changeBtnDisplayName, btnDisplayNameBlurEvent } =
useButtonsSetting();
//
const fieldsPermissionEl = ref<any[]>([]);
const { formType, fieldsPermissionConfig, getNodeConfigFormFields } =
useFormFieldsPermission(FieldPermissionType.READ);
//
const approveType = ref({ value: ApproveType.USER });
//
const signEnable = ref({ value: false });
//
const reasonRequire = ref({ value: false });
const elExtensionElements = ref<any>();
const otherExtensions = ref<any>();
const bpmnElement = ref<any>();
const bpmnInstances = () => (window as any)?.bpmnInstances;
const resetCustomConfigList = () => {
bpmnElement.value = bpmnInstances().bpmnElement;
// 退
returnTaskList.value = findAllPredecessorsExcludingStart(
bpmnElement.value.id,
bpmnInstances().modeler,
);
//
elExtensionElements.value =
bpmnElement.value.businessObject?.extensionElements ??
bpmnInstances().moddle.create('bpmn:ExtensionElements', { values: [] });
//
approveType.value =
elExtensionElements.value.values?.filter(
(ex: any) => ex.$type === `${prefix}:ApproveType`,
)?.[0] ||
bpmnInstances().moddle.create(`${prefix}:ApproveType`, {
value: ApproveType.USER,
});
//
assignStartUserHandlerTypeEl.value =
elExtensionElements.value.values?.filter(
(ex: any) => ex.$type === `${prefix}:AssignStartUserHandlerType`,
)?.[0] ||
bpmnInstances().moddle.create(`${prefix}:AssignStartUserHandlerType`, {
value: 1,
});
assignStartUserHandlerType.value = assignStartUserHandlerTypeEl.value.value;
//
rejectHandlerTypeEl.value =
elExtensionElements.value.values?.filter(
(ex: any) => ex.$type === `${prefix}:RejectHandlerType`,
)?.[0] ||
bpmnInstances().moddle.create(`${prefix}:RejectHandlerType`, { value: 1 });
rejectHandlerType.value = rejectHandlerTypeEl.value.value;
returnNodeIdEl.value =
elExtensionElements.value.values?.filter(
(ex: any) => ex.$type === `${prefix}:RejectReturnTaskId`,
)?.[0] ||
bpmnInstances().moddle.create(`${prefix}:RejectReturnTaskId`, {
value: '',
});
returnNodeId.value = returnNodeIdEl.value.value;
//
assignEmptyHandlerTypeEl.value =
elExtensionElements.value.values?.filter(
(ex: any) => ex.$type === `${prefix}:AssignEmptyHandlerType`,
)?.[0] ||
bpmnInstances().moddle.create(`${prefix}:AssignEmptyHandlerType`, {
value: 1,
});
assignEmptyHandlerType.value = assignEmptyHandlerTypeEl.value.value;
assignEmptyUserIdsEl.value =
elExtensionElements.value.values?.filter(
(ex: any) => ex.$type === `${prefix}:AssignEmptyUserIds`,
)?.[0] ||
bpmnInstances().moddle.create(`${prefix}:AssignEmptyUserIds`, {
value: '',
});
assignEmptyUserIds.value = assignEmptyUserIdsEl.value.value
?.split(',')
.map((item: string) => {
//
const num = Number(item);
return num > Number.MAX_SAFE_INTEGER || num < -Number.MAX_SAFE_INTEGER
? item
: num;
});
//
buttonsSettingEl.value = elExtensionElements.value.values?.filter(
(ex: any) => ex.$type === `${prefix}:ButtonsSetting`,
);
if (buttonsSettingEl.value.length === 0) {
DEFAULT_BUTTON_SETTING.forEach((item) => {
buttonsSettingEl.value.push(
bpmnInstances().moddle.create(`${prefix}:ButtonsSetting`, {
'flowable:id': item.id,
'flowable:displayName': item.displayName,
'flowable:enable': item.enable,
}),
);
});
}
//
if (formType.value === BpmModelFormType.NORMAL) {
const fieldsPermissionList = elExtensionElements.value.values?.filter(
(ex: any) => ex.$type === `${prefix}:FieldsPermission`,
);
fieldsPermissionEl.value = [];
getNodeConfigFormFields();
fieldsPermissionConfig.value.forEach((element: any) => {
element.permission =
fieldsPermissionList?.find((obj: any) => obj.field === element.field)
?.permission ?? '1';
fieldsPermissionEl.value.push(
bpmnInstances().moddle.create(`${prefix}:FieldsPermission`, element),
);
});
}
//
signEnable.value =
elExtensionElements.value.values?.filter(
(ex: any) => ex.$type === `${prefix}:SignEnable`,
)?.[0] ||
bpmnInstances().moddle.create(`${prefix}:SignEnable`, { value: false });
//
reasonRequire.value =
elExtensionElements.value.values?.filter(
(ex: any) => ex.$type === `${prefix}:ReasonRequire`,
)?.[0] ||
bpmnInstances().moddle.create(`${prefix}:ReasonRequire`, { value: false });
// 便
otherExtensions.value =
elExtensionElements.value.values?.filter(
(ex: any) =>
ex.$type !== `${prefix}:AssignStartUserHandlerType` &&
ex.$type !== `${prefix}:RejectHandlerType` &&
ex.$type !== `${prefix}:RejectReturnTaskId` &&
ex.$type !== `${prefix}:AssignEmptyHandlerType` &&
ex.$type !== `${prefix}:AssignEmptyUserIds` &&
ex.$type !== `${prefix}:ButtonsSetting` &&
ex.$type !== `${prefix}:FieldsPermission` &&
ex.$type !== `${prefix}:ApproveType` &&
ex.$type !== `${prefix}:SignEnable` &&
ex.$type !== `${prefix}:ReasonRequire`,
) ?? [];
//
updateElementExtensions();
};
const updateAssignStartUserHandlerType = () => {
assignStartUserHandlerTypeEl.value.value = assignStartUserHandlerType.value;
updateElementExtensions();
};
const updateRejectHandlerType = () => {
rejectHandlerTypeEl.value.value = rejectHandlerType.value;
returnNodeId.value = returnTaskList.value[0]?.id;
returnNodeIdEl.value.value = returnNodeId.value;
updateElementExtensions();
};
const updateReturnNodeId = () => {
returnNodeIdEl.value.value = returnNodeId.value;
updateElementExtensions();
};
const updateAssignEmptyHandlerType = () => {
assignEmptyHandlerTypeEl.value.value = assignEmptyHandlerType.value;
updateElementExtensions();
};
const updateAssignEmptyUserIds = () => {
assignEmptyUserIdsEl.value.value = assignEmptyUserIds.value.toString();
updateElementExtensions();
};
const updateElementExtensions = () => {
const extensions = bpmnInstances().moddle.create('bpmn:ExtensionElements', {
values: [
...otherExtensions.value,
assignStartUserHandlerTypeEl.value,
rejectHandlerTypeEl.value,
returnNodeIdEl.value,
assignEmptyHandlerTypeEl.value,
assignEmptyUserIdsEl.value,
approveType.value,
...buttonsSettingEl.value,
...fieldsPermissionEl.value,
signEnable.value,
reasonRequire.value,
],
});
bpmnInstances().modeling.updateProperties(toRaw(bpmnElement.value), {
extensionElements: extensions,
});
};
watch(
() => props.id,
(val) => {
val &&
val.length > 0 &&
nextTick(() => {
resetCustomConfigList();
});
},
{ immediate: true },
);
function findAllPredecessorsExcludingStart(elementId: string, modeler: any) {
const elementRegistry = modeler.get('elementRegistry');
const allConnections = elementRegistry.filter(
(element: any) => element.type === 'bpmn:SequenceFlow',
);
const predecessors = new Set(); // 使 Set
const visited = new Set(); // 访
//
function isStartEvent(element: any) {
return element.type === 'bpmn:StartEvent';
}
function findPredecessorsRecursively(element: any) {
// 访
if (visited.has(element)) {
return;
}
// 访
visited.add(element);
//
const incomingConnections = allConnections.filter(
(connection: any) => connection.target === element,
);
incomingConnections.forEach((connection: any) => {
const source = connection.source; //
//
if (!isStartEvent(source)) {
predecessors.add(source.businessObject);
//
findPredecessorsRecursively(source);
}
});
}
const targetElement = elementRegistry.get(elementId);
if (targetElement) {
findPredecessorsRecursively(targetElement);
}
return [...predecessors]; //
}
function useButtonsSetting() {
const buttonsSetting = ref<ButtonSetting[]>();
//
const btnDisplayNameEdit = ref<boolean[]>([]);
const changeBtnDisplayName = (index: number) => {
btnDisplayNameEdit.value[index] = true;
};
const btnDisplayNameBlurEvent = (index: number) => {
btnDisplayNameEdit.value[index] = false;
const buttonItem = buttonsSetting.value?.[index];
if (buttonItem) {
buttonItem.displayName =
buttonItem.displayName || OPERATION_BUTTON_NAME.get(buttonItem.id)!;
}
};
return {
buttonsSetting,
btnDisplayNameEdit,
changeBtnDisplayName,
btnDisplayNameBlurEvent,
};
}
/** 批量更新权限 */
// TODO @lesan idea fix
const updatePermission = (type: string) => {
fieldsPermissionEl.value.forEach((field: any) => {
if (type === 'READ') {
field.permission = FieldPermissionType.READ;
} else if (type === 'WRITE') {
field.permission = FieldPermissionType.WRITE;
} else {
field.permission = FieldPermissionType.NONE;
}
});
};
const userOptions = ref<SystemUserApi.User[]>([]); //
onMounted(async () => {
//
userOptions.value = await getSimpleUserList();
});
</script>
<template>
<div>
<Divider orientation="left">审批类型</Divider>
<Form.Item name="approveType" label="审批类型">
<RadioGroup v-model:value="approveType.value">
<Radio
v-for="(item, index) in APPROVE_TYPE"
:key="index"
:value="item.value"
>
{{ item.label }}
</Radio>
</RadioGroup>
</Form.Item>
<Divider orientation="left">审批人拒绝时</Divider>
<Form.Item name="rejectHandlerType" label="处理方式">
<RadioGroup
v-model:value="rejectHandlerType"
:disabled="returnTaskList.length === 0"
@change="updateRejectHandlerType"
>
<div class="flex-col">
<div v-for="(item, index) in REJECT_HANDLER_TYPES" :key="index">
<Radio :key="item.value" :value="item.value">
{{ item.label }}
</Radio>
</div>
</div>
</RadioGroup>
</Form.Item>
<Form.Item
v-if="rejectHandlerType === RejectHandlerType.RETURN_USER_TASK"
name="returnNodeId"
label="驳回节点"
>
<Select
v-model:value="returnNodeId"
allow-clear
style="width: 100%"
@change="updateReturnNodeId"
placeholder="请选择驳回节点"
>
<SelectOption
v-for="item in returnTaskList"
:key="item.id"
:value="item.id"
>
{{ item.name }}
</SelectOption>
</Select>
</Form.Item>
<Divider orientation="left">审批人为空时</Divider>
<Form.Item prop="assignEmptyHandlerType">
<RadioGroup
v-model:value="assignEmptyHandlerType"
@change="updateAssignEmptyHandlerType"
>
<div class="flex-col">
<div v-for="(item, index) in ASSIGN_EMPTY_HANDLER_TYPES" :key="index">
<Radio :key="item.value" :value="item.value">
{{ item.label }}
</Radio>
</div>
</div>
</RadioGroup>
</Form.Item>
<Form.Item
v-if="assignEmptyHandlerType === AssignEmptyHandlerType.ASSIGN_USER"
label="指定用户"
prop="assignEmptyHandlerUserIds"
>
<Select
v-model:value="assignEmptyUserIds"
allow-clear
mode="multiple"
style="width: 100%"
@change="updateAssignEmptyUserIds"
>
<SelectOption
v-for="item in userOptions"
:key="item.id"
:value="item.id"
>
{{ item.nickname }}
</SelectOption>
</Select>
</Form.Item>
<Divider orientation="left">审批人与提交人为同一人时</Divider>
<RadioGroup
v-model:value="assignStartUserHandlerType"
@change="updateAssignStartUserHandlerType"
>
<div class="flex-col">
<div
v-for="(item, index) in ASSIGN_START_USER_HANDLER_TYPES"
:key="index"
>
<Radio :key="item.value" :value="item.value">
{{ item.label }}
</Radio>
</div>
</div>
</RadioGroup>
<Divider orientation="left">操作按钮</Divider>
<div class="button-setting-pane">
<div class="button-setting-title">
<div class="button-title-label">操作按钮</div>
<div class="button-title-label pl-4">显示名称</div>
<div class="button-title-label">启用</div>
</div>
<div
class="button-setting-item"
v-for="(item, index) in buttonsSettingEl"
:key="index"
>
<div class="button-setting-item-label">
{{ OPERATION_BUTTON_NAME.get(item.id) }}
</div>
<div class="button-setting-item-label">
<input
type="text"
class="editable-title-input"
@blur="btnDisplayNameBlurEvent(index)"
v-mounted-focus
v-model="item.displayName"
:placeholder="item.displayName"
v-if="btnDisplayNameEdit[index]"
/>
<Button v-else type="text" @click="changeBtnDisplayName(index)">
{{ item.displayName }}
</Button>
</div>
<div class="button-setting-item-label">
<Switch v-model:checked="item.enable" />
</div>
</div>
</div>
<Divider orientation="left">字段权限</Divider>
<div class="field-setting-pane" v-if="formType === BpmModelFormType.NORMAL">
<div class="field-permit-title">
<div class="setting-title-label first-title">字段名称</div>
<div class="other-titles">
<span
class="setting-title-label cursor-pointer"
@click="updatePermission('READ')"
>只读
</span>
<span
class="setting-title-label cursor-pointer"
@click="updatePermission('WRITE')"
>
可编辑
</span>
<span
class="setting-title-label cursor-pointer"
@click="updatePermission('NONE')"
>隐藏
</span>
</div>
</div>
<div
class="field-setting-item"
v-for="(item, index) in fieldsPermissionEl"
:key="index"
>
<div class="field-setting-item-label">{{ item.title }}</div>
<RadioGroup
class="field-setting-item-group"
v-model:value="item.permission"
>
<div class="item-radio-wrap">
<Radio
:value="FieldPermissionType.READ"
size="large"
@change="updateElementExtensions"
>
<span></span>
</Radio>
</div>
<div class="item-radio-wrap">
<Radio
:value="FieldPermissionType.WRITE"
size="large"
@change="updateElementExtensions"
>
<span></span>
</Radio>
</div>
<div class="item-radio-wrap">
<Radio
:value="FieldPermissionType.NONE"
size="large"
@change="updateElementExtensions"
>
<span></span>
</Radio>
</div>
</RadioGroup>
</div>
</div>
<Divider orientation="left">是否需要签名</Divider>
<Form.Item prop="signEnable">
<Switch
v-model:checked="signEnable.value"
checked-children="是"
un-checked-children="否"
@change="updateElementExtensions"
/>
</Form.Item>
<Divider orientation="left">审批意见</Divider>
<Form.Item prop="reasonRequire">
<Switch
v-model:checked="reasonRequire.value"
checked-children="必填"
un-checked-children="非必填"
@change="updateElementExtensions"
/>
</Form.Item>
</div>
</template>
<style lang="scss" scoped>
.button-setting-pane {
display: flex;
flex-direction: column;
margin-top: 8px;
font-size: 14px;
.button-setting-desc {
padding-right: 8px;
margin-bottom: 16px;
font-size: 16px;
font-weight: 700;
}
.button-setting-title {
display: flex;
align-items: center;
justify-content: space-between;
height: 45px;
padding-left: 12px;
background-color: #f8fafc0a;
border: 1px solid #1f38581a;
& > :first-child {
width: 100px !important;
text-align: left !important;
}
& > :last-child {
text-align: center !important;
}
.button-title-label {
width: 150px;
font-size: 13px;
font-weight: 700;
color: #000;
text-align: left;
}
}
.button-setting-item {
display: flex;
align-items: center;
justify-content: space-between;
height: 38px;
padding-left: 12px;
border: 1px solid #1f38581a;
border-top: 0;
& > :first-child {
width: 100px !important;
}
& > :last-child {
text-align: center !important;
}
.button-setting-item-label {
width: 150px;
overflow: hidden;
text-overflow: ellipsis;
text-align: left;
white-space: nowrap;
}
.editable-title-input {
max-width: 130px;
height: 24px;
margin-left: 4px;
line-height: 24px;
border: 1px solid #d9d9d9;
border-radius: 4px;
transition: all 0.3s;
&:focus {
outline: 0;
border-color: #40a9ff;
box-shadow: 0 0 0 2px rgb(24 144 255 / 20%);
}
}
}
}
.field-setting-pane {
display: flex;
flex-direction: column;
font-size: 14px;
.field-setting-desc {
padding-right: 8px;
margin-bottom: 16px;
font-size: 16px;
font-weight: 700;
}
.field-permit-title {
display: flex;
align-items: center;
justify-content: space-between;
height: 45px;
padding-left: 12px;
line-height: 45px;
background-color: #f8fafc0a;
border: 1px solid #1f38581a;
.first-title {
text-align: left !important;
}
.other-titles {
display: flex;
justify-content: space-between;
}
.setting-title-label {
display: inline-block;
width: 100px;
padding: 5px 0;
font-size: 13px;
font-weight: 700;
color: #000;
text-align: center;
}
}
.field-setting-item {
display: flex;
align-items: center;
justify-content: space-between;
height: 38px;
padding-left: 12px;
border: 1px solid #1f38581a;
border-top: 0;
.field-setting-item-label {
display: inline-block;
width: 100px;
min-height: 16px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
cursor: text;
}
.field-setting-item-group {
display: flex;
justify-content: space-between;
.item-radio-wrap {
display: inline-block;
width: 100px;
text-align: center;
}
}
}
}
</style>

View File

@ -0,0 +1,13 @@
import BoundaryEventTimer from './components/BoundaryEventTimer.vue';
import UserTaskCustomConfig from './components/UserTaskCustomConfig.vue';
export const CustomConfigMap = {
UserTask: {
name: '用户任务',
component: UserTaskCustomConfig,
},
BoundaryEventTimerEventDefinition: {
name: '定时边界事件(非中断)',
component: BoundaryEventTimer,
},
};

View File

@ -0,0 +1,238 @@
<script lang="ts" setup>
import { nextTick, onBeforeUnmount, ref, toRaw, watch } from 'vue';
import { Form, Input, Select } from 'ant-design-vue';
defineOptions({ name: 'FlowCondition' });
const props = defineProps({
businessObject: {
type: Object,
default: () => ({}),
},
type: {
type: String,
default: '',
},
});
const { TextArea } = Input;
const flowConditionForm = ref<any>({});
const bpmnElement = ref();
const bpmnElementSource = ref();
const bpmnElementSourceRef = ref();
const flowConditionRef = ref();
const bpmnInstances = () => (window as any)?.bpmnInstances;
const resetFlowCondition = () => {
bpmnElement.value = bpmnInstances().bpmnElement;
bpmnElementSource.value = bpmnElement.value.source;
bpmnElementSourceRef.value = bpmnElement.value.businessObject.sourceRef;
// typedefault
flowConditionForm.value = { type: 'default' };
if (
bpmnElementSourceRef.value &&
bpmnElementSourceRef.value.default &&
bpmnElementSourceRef.value.default.id === bpmnElement.value.id
) {
flowConditionForm.value = { type: 'default' };
} else if (bpmnElement.value.businessObject.conditionExpression) {
//
const conditionExpression =
bpmnElement.value.businessObject.conditionExpression;
flowConditionForm.value = { ...conditionExpression, type: 'condition' };
// resource
if (flowConditionForm.value.resource) {
// this.$set(this.flowConditionForm, "conditionType", "script");
// this.$set(this.flowConditionForm, "scriptType", "externalScript");
flowConditionForm.value.conditionType = 'script';
flowConditionForm.value.scriptType = 'externalScript';
return;
}
if (conditionExpression.language) {
// this.$set(this.flowConditionForm, "conditionType", "script");
// this.$set(this.flowConditionForm, "scriptType", "inlineScript");
flowConditionForm.value.conditionType = 'script';
flowConditionForm.value.scriptType = 'inlineScript';
return;
}
// this.$set(this.flowConditionForm, "conditionType", "expression");
flowConditionForm.value.conditionType = 'expression';
} else {
//
flowConditionForm.value = { type: 'normal' };
}
};
const updateFlowType = (flowType: any) => {
//
if (flowType === 'condition') {
flowConditionRef.value = bpmnInstances().moddle.create(
'bpmn:FormalExpression',
);
bpmnInstances().modeling.updateProperties(toRaw(bpmnElement.value), {
conditionExpression: flowConditionRef.value,
});
return;
}
//
if (flowType === 'default') {
bpmnInstances().modeling.updateProperties(toRaw(bpmnElement.value), {
conditionExpression: null,
});
bpmnInstances().modeling.updateProperties(toRaw(bpmnElementSource.value), {
default: toRaw(bpmnElement.value),
});
return;
}
// 线
if (
bpmnElementSourceRef.value.default &&
bpmnElementSourceRef.value.default.id === bpmnElement.value.id
) {
bpmnInstances().modeling.updateProperties(toRaw(bpmnElementSource.value), {
default: null,
});
}
bpmnInstances().modeling.updateProperties(toRaw(bpmnElement.value), {
conditionExpression: null,
});
};
const updateFlowCondition = () => {
const { conditionType, scriptType, body, resource, language } =
flowConditionForm.value;
let condition;
if (conditionType === 'expression') {
condition = bpmnInstances().moddle.create('bpmn:FormalExpression', {
body,
});
} else {
if (scriptType === 'inlineScript') {
condition = bpmnInstances().moddle.create('bpmn:FormalExpression', {
body,
language,
});
// this.$set(this.flowConditionForm, "resource", "");
flowConditionForm.value.resource = '';
} else {
// this.$set(this.flowConditionForm, "body", "");
flowConditionForm.value.body = '';
condition = bpmnInstances().moddle.create('bpmn:FormalExpression', {
resource,
language,
});
}
}
bpmnInstances().modeling.updateProperties(toRaw(bpmnElement.value), {
conditionExpression: condition,
});
};
onBeforeUnmount(() => {
bpmnElement.value = null;
bpmnElementSource.value = null;
bpmnElementSourceRef.value = null;
});
watch(
() => props.businessObject,
(_) => {
// console.log(val, 'val');
nextTick(() => {
resetFlowCondition();
});
},
{
immediate: true,
},
);
</script>
<template>
<div class="panel-tab__content">
<Form
:model="flowConditionForm"
:label-col="{ span: 6 }"
:wrapper-col="{ span: 18 }"
>
<Form.Item label="流转类型">
<Select v-model:value="flowConditionForm.type" @change="updateFlowType">
<Select.Option value="normal">普通流转路径</Select.Option>
<Select.Option value="default">默认流转路径</Select.Option>
<Select.Option value="condition">条件流转路径</Select.Option>
</Select>
</Form.Item>
<Form.Item
label="条件格式"
v-if="flowConditionForm.type === 'condition'"
key="condition"
>
<Select v-model:value="flowConditionForm.conditionType">
<Select.Option value="expression">表达式</Select.Option>
<Select.Option value="script">脚本</Select.Option>
</Select>
</Form.Item>
<Form.Item
label="表达式"
v-if="
flowConditionForm.conditionType &&
flowConditionForm.conditionType === 'expression'
"
key="express"
>
<Input
v-model:value="flowConditionForm.body"
style="width: 192px"
allow-clear
@change="updateFlowCondition"
/>
</Form.Item>
<template
v-if="
flowConditionForm.conditionType &&
flowConditionForm.conditionType === 'script'
"
>
<Form.Item label="脚本语言" key="language">
<Input
v-model:value="flowConditionForm.language"
allow-clear
@change="updateFlowCondition"
/>
</Form.Item>
<Form.Item label="脚本类型" key="scriptType">
<Select v-model:value="flowConditionForm.scriptType">
<Select.Option value="inlineScript">内联脚本</Select.Option>
<Select.Option value="externalScript">外部脚本</Select.Option>
</Select>
</Form.Item>
<Form.Item
label="脚本"
v-if="flowConditionForm.scriptType === 'inlineScript'"
key="body"
>
<TextArea
v-model:value="flowConditionForm.body"
:auto-size="{ minRows: 2, maxRows: 6 }"
allow-clear
@change="updateFlowCondition"
/>
</Form.Item>
<Form.Item
label="资源地址"
v-if="flowConditionForm.scriptType === 'externalScript'"
key="resource"
>
<Input
v-model:value="flowConditionForm.resource"
allow-clear
@change="updateFlowCondition"
/>
</Form.Item>
</template>
</Form>
</div>
</template>

View File

@ -0,0 +1,538 @@
<script lang="ts" setup>
import { computed, inject, nextTick, onMounted, ref, toRaw, watch } from 'vue';
import { cloneDeep } from '@vben/utils';
import { Form, FormItem, Select } from 'ant-design-vue';
import { getFormSimpleList } from '#/api/bpm/form';
defineOptions({ name: 'ElementForm' });
const props = defineProps({
id: {
type: String,
default: '',
},
type: {
type: String,
default: '',
},
});
const prefix = inject('prefix');
const formKey = ref<number | string | undefined>(undefined);
const businessKey = ref('');
const optionModelTitle = ref('');
const fieldList = ref<any[]>([]);
const formFieldForm = ref<any>({});
const fieldType = ref({
long: '长整型',
string: '字符串',
boolean: '布尔类',
date: '日期类',
enum: '枚举类',
custom: '自定义类型',
});
const formFieldIndex = ref(-1); // -1
const formFieldOptionIndex = ref(-1); // -1
const fieldModelVisible = ref(false);
const fieldOptionModelVisible = ref(false);
const fieldOptionForm = ref<any>({}); //
const fieldOptionType = ref(''); //
const fieldEnumList = ref<any[]>([]); //
const fieldConstraintsList = ref<any[]>([]); //
const fieldPropertiesList = ref<any[]>([]); //
const bpmnELement = ref();
const elExtensionElements = ref();
const formData = ref();
const otherExtensions = ref();
const bpmnInstances = () => (window as any)?.bpmnInstances;
const resetFormList = () => {
bpmnELement.value = bpmnInstances().bpmnElement;
formKey.value = bpmnELement.value.businessObject.formKey;
// if (formKey.value?.length > 0) {
// formKey.value = parseInt(formKey.value)
// }
//
elExtensionElements.value =
bpmnELement.value.businessObject.get('extensionElements') ||
bpmnInstances().moddle.create('bpmn:ExtensionElements', { values: [] });
//
formData.value =
elExtensionElements.value.values.find(
(ex: any) => ex.$type === `${prefix}:FormData`,
) || bpmnInstances().moddle.create(`${prefix}:FormData`, { fields: [] });
// businessKey formData
businessKey.value = formData.value.businessKey;
// 便
otherExtensions.value = elExtensionElements.value.values.filter(
(ex: any) => ex.$type !== `${prefix}:FormData`,
);
//
fieldList.value = cloneDeep(formData.value.fields || []);
//
updateElementExtensions();
};
const updateElementFormKey = () => {
bpmnInstances().modeling.updateProperties(toRaw(bpmnELement.value), {
formKey: formKey.value,
});
};
const _updateElementBusinessKey = () => {
bpmnInstances().modeling.updateModdleProperties(
toRaw(bpmnELement.value),
formData.value,
{
businessKey: businessKey.value,
},
);
};
// type
const _changeFieldTypeType = (type: any) => {
formFieldForm.value.type = type === 'custom' ? '' : type;
};
//
const _openFieldForm = (field: any, index: any) => {
formFieldIndex.value = index;
if (index === -1) {
formFieldForm.value = {};
//
fieldEnumList.value = [];
//
fieldConstraintsList.value = [];
//
fieldPropertiesList.value = [];
} else {
const FieldObject = formData.value.fields[index];
formFieldForm.value = cloneDeep(field);
//
// this.$set(this.formFieldForm, "typeType", !this.fieldType[field.type] ? "custom" : field.type);
formFieldForm.value.typeType = fieldType.value[
field.type as keyof typeof fieldType.value
]
? field.type
: 'custom';
//
field.type === 'enum' &&
(fieldEnumList.value = cloneDeep(FieldObject?.values || []));
//
fieldConstraintsList.value = cloneDeep(
FieldObject?.validation?.constraints || [],
);
//
fieldPropertiesList.value = cloneDeep(
FieldObject?.properties?.values || [],
);
}
fieldModelVisible.value = true;
};
//
const _openFieldOptionForm = (option: any, index: any, type: any) => {
fieldOptionModelVisible.value = true;
fieldOptionType.value = type;
formFieldOptionIndex.value = index;
if (type === 'property') {
fieldOptionForm.value = option ? cloneDeep(option) : {};
return (optionModelTitle.value = '属性配置');
}
if (type === 'enum') {
fieldOptionForm.value = option ? cloneDeep(option) : {};
return (optionModelTitle.value = '枚举值配置');
}
fieldOptionForm.value = option ? cloneDeep(option) : {};
return (optionModelTitle.value = '约束条件配置');
};
//
const _saveFieldOption = () => {
if (formFieldOptionIndex.value === -1) {
if (fieldOptionType.value === 'property') {
fieldPropertiesList.value.push(fieldOptionForm.value);
}
if (fieldOptionType.value === 'constraint') {
fieldConstraintsList.value.push(fieldOptionForm.value);
}
if (fieldOptionType.value === 'enum') {
fieldEnumList.value.push(fieldOptionForm.value);
}
} else {
fieldOptionType.value === 'property' &&
fieldPropertiesList.value.splice(
formFieldOptionIndex.value,
1,
fieldOptionForm.value,
);
fieldOptionType.value === 'constraint' &&
fieldConstraintsList.value.splice(
formFieldOptionIndex.value,
1,
fieldOptionForm.value,
);
fieldOptionType.value === 'enum' &&
fieldEnumList.value.splice(
formFieldOptionIndex.value,
1,
fieldOptionForm.value,
);
}
fieldOptionModelVisible.value = false;
fieldOptionForm.value = {};
};
//
const _saveField = () => {
const { id, type, label, defaultValue, datePattern } = formFieldForm.value;
const Field = bpmnInstances().moddle.create(`${prefix}:FormField`, {
id,
type,
label,
});
defaultValue && (Field.defaultValue = defaultValue);
datePattern && (Field.datePattern = datePattern);
//
if (fieldPropertiesList.value && fieldPropertiesList.value.length > 0) {
const fieldPropertyList = fieldPropertiesList.value.map((fp: any) => {
return bpmnInstances().moddle.create(`${prefix}:Property`, {
id: fp.id,
value: fp.value,
});
});
Field.properties = bpmnInstances().moddle.create(`${prefix}:Properties`, {
values: fieldPropertyList,
});
}
//
if (fieldConstraintsList.value && fieldConstraintsList.value.length > 0) {
const fieldConstraintList = fieldConstraintsList.value.map((fc: any) => {
return bpmnInstances().moddle.create(`${prefix}:Constraint`, {
name: fc.name,
config: fc.config,
});
});
Field.validation = bpmnInstances().moddle.create(`${prefix}:Validation`, {
constraints: fieldConstraintList,
});
}
//
if (fieldEnumList.value && fieldEnumList.value.length > 0) {
Field.values = fieldEnumList.value.map((fe: any) => {
return bpmnInstances().moddle.create(`${prefix}:Value`, {
name: fe.name,
id: fe.id,
});
});
}
//
if (formFieldIndex.value === -1) {
fieldList.value.push(formFieldForm.value);
formData.value.fields.push(Field);
} else {
fieldList.value.splice(formFieldIndex.value, 1, formFieldForm.value);
formData.value.fields.splice(formFieldIndex.value, 1, Field);
}
updateElementExtensions();
fieldModelVisible.value = false;
};
//
const _removeFieldOptionItem = (_option: any, index: any, type: any) => {
// console.log(option, 'option')
if (type === 'property') {
fieldPropertiesList.value.splice(index, 1);
return;
}
if (type === 'enum') {
fieldEnumList.value.splice(index, 1);
return;
}
fieldConstraintsList.value.splice(index, 1);
};
//
const _removeField = (field: any, index: any) => {
console.warn(field, 'field');
fieldList.value.splice(index, 1);
formData.value.fields.splice(index, 1);
updateElementExtensions();
};
const updateElementExtensions = () => {
//
const newElExtensionElements = bpmnInstances().moddle.create(
`bpmn:ExtensionElements`,
{
values: [...otherExtensions.value, formData.value],
},
);
//
bpmnInstances().modeling.updateProperties(toRaw(bpmnELement.value), {
extensionElements: newElExtensionElements,
});
};
const formList = ref<any[]>([]); //
const formOptions = computed(() => {
return formList.value.map((form: any) => ({
value: form.id,
label: form.name,
}));
});
onMounted(async () => {
formList.value = await getFormSimpleList();
formKey.value = formKey.value
? Number.parseInt(formKey.value as string)
: undefined;
});
watch(
() => props.id,
(val: any) => {
val &&
val.length > 0 &&
nextTick(() => {
resetFormList();
});
},
{ immediate: true },
);
</script>
<template>
<div class="panel-tab__content">
<Form :label-col="{ style: { width: '80px' } }">
<FormItem label="流程表单">
<!-- <Input v-model:value="formKey" @change="updateElementFormKey" />-->
<Select
v-model:value="formKey"
allow-clear
@change="updateElementFormKey"
:options="formOptions"
/>
</FormItem>
<FormItem label="业务标识">
<Select
v-model:value="businessKey"
@change="_updateElementBusinessKey"
allow-clear
>
<Select.Option v-for="i in fieldList" :key="i.id" :value="i.id">
{{ i.label }}
</Select.Option>
<Select.Option value=""></Select.Option>
</Select>
</FormItem>
</Form>
<!--字段列表-->
<!-- <div class="element-property list-property">-->
<!-- <Divider><Icon icon="ep:coin" /> 表单字段</Divider>-->
<!-- <Table :data-source="fieldList" :scroll="{ y: 240 }" bordered>-->
<!-- <TableColumn title="序号" type="index" width="50px" />-->
<!-- <TableColumn title="字段名称" dataIndex="label" width="80px" :ellipsis="true" />-->
<!-- <TableColumn-->
<!-- title="字段类型"-->
<!-- dataIndex="type"-->
<!-- width="80px"-->
<!-- :customRender="({ text }) => fieldType[text] || text"-->
<!-- :ellipsis="true"-->
<!-- />-->
<!-- <TableColumn-->
<!-- title="默认值"-->
<!-- dataIndex="defaultValue"-->
<!-- width="80px"-->
<!-- :ellipsis="true"-->
<!-- />-->
<!-- <TableColumn title="操作" width="90px">-->
<!-- <template #default="scope">-->
<!-- <Button type="link" @click="openFieldForm(scope, scope.$index)">-->
<!-- 编辑-->
<!-- </Button>-->
<!-- <Divider type="vertical" />-->
<!-- <Button-->
<!-- type="link"-->
<!-- danger-->
<!-- @click="removeField(scope, scope.$index)"-->
<!-- >-->
<!-- 移除-->
<!-- </Button>-->
<!-- </template>-->
<!-- </TableColumn>-->
<!-- </Table>-->
<!-- </div>-->
<!-- <div class="element-drawer__button">-->
<!-- <Button type="primary" @click="openFieldForm(null, -1)">添加字段</Button>-->
<!-- </div>-->
<!--字段配置侧边栏-->
<!-- <Drawer-->
<!-- v-model:open="fieldModelVisible"-->
<!-- title="字段配置"-->
<!-- :width="`${width}px`"-->
<!-- destroyOnClose-->
<!-- >-->
<!-- <Form :model="formFieldForm" :label-col="{ style: { width: '90px' } }">-->
<!-- <FormItem label="字段ID">-->
<!-- <Input v-model:value="formFieldForm.id" allowClear />-->
<!-- </FormItem>-->
<!-- <FormItem label="类型">-->
<!-- <Select-->
<!-- v-model:value="formFieldForm.typeType"-->
<!-- placeholder="请选择字段类型"-->
<!-- allowClear-->
<!-- @change="changeFieldTypeType"-->
<!-- >-->
<!-- <SelectOption v-for="(value, key) of fieldType" :key="key" :value="key">{{ value }}</SelectOption>-->
<!-- </Select>-->
<!-- </FormItem>-->
<!-- <FormItem label="类型名称" v-if="formFieldForm.typeType === 'custom'">-->
<!-- <Input v-model:value="formFieldForm.type" allowClear />-->
<!-- </FormItem>-->
<!-- <FormItem label="名称">-->
<!-- <Input v-model:value="formFieldForm.label" allowClear />-->
<!-- </FormItem>-->
<!-- <FormItem label="时间格式" v-if="formFieldForm.typeType === 'date'">-->
<!-- <Input v-model:value="formFieldForm.datePattern" allowClear />-->
<!-- </FormItem>-->
<!-- <FormItem label="默认值">-->
<!-- <Input v-model:value="formFieldForm.defaultValue" allowClear />-->
<!-- </FormItem>-->
<!-- </Form>-->
<!-- &lt;!&ndash; 枚举值设置 &ndash;&gt;-->
<!-- <template v-if="formFieldForm.type === 'enum'">-->
<!-- <Divider key="enum-divider" />-->
<!-- <p class="listener-filed__title" key="enum-title">-->
<!-- <span><Icon icon="ep:menu" />枚举值列表</span>-->
<!-- <Button type="primary" @click="openFieldOptionForm(null, -1, 'enum')"-->
<!-- >添加枚举值</Button-->
<!-- >-->
<!-- </p>-->
<!-- <Table :data-source="fieldEnumList" key="enum-table" :scroll="{ y: 240 }" bordered>-->
<!-- <TableColumn title="序号" width="50px" type="index" />-->
<!-- <TableColumn title="枚举值编号" dataIndex="id" width="100px" :ellipsis="true" />-->
<!-- <TableColumn title="枚举值名称" dataIndex="name" width="100px" :ellipsis="true" />-->
<!-- <TableColumn title="操作" width="90px">-->
<!-- <template #default="scope">-->
<!-- <Button-->
<!-- type="link"-->
<!-- @click="openFieldOptionForm(scope, scope.$index, 'enum')"-->
<!-- >-->
<!-- 编辑-->
<!-- </Button>-->
<!-- <Divider type="vertical" />-->
<!-- <Button-->
<!-- type="link"-->
<!-- danger-->
<!-- @click="removeFieldOptionItem(scope, scope.$index, 'enum')"-->
<!-- >-->
<!-- 移除-->
<!-- </Button>-->
<!-- </template>-->
<!-- </TableColumn>-->
<!-- </Table>-->
<!-- </template>-->
<!-- &lt;!&ndash; 校验规则 &ndash;&gt;-->
<!-- <Divider key="validation-divider" />-->
<!-- <p class="listener-filed__title" key="validation-title">-->
<!-- <span><Icon icon="ep:menu" />约束条件列表</span>-->
<!-- <Button type="primary" @click="openFieldOptionForm(null, -1, 'constraint')"-->
<!-- >添加约束</Button-->
<!-- >-->
<!-- </p>-->
<!-- <Table :data-source="fieldConstraintsList" key="validation-table" :scroll="{ y: 240 }" bordered>-->
<!-- <TableColumn title="序号" width="50px" type="index" />-->
<!-- <TableColumn title="约束名称" dataIndex="name" width="100px" :ellipsis="true" />-->
<!-- <TableColumn title="约束配置" dataIndex="config" width="100px" :ellipsis="true" />-->
<!-- <TableColumn title="操作" width="90px">-->
<!-- <template #default="scope">-->
<!-- <Button-->
<!-- type="link"-->
<!-- @click="openFieldOptionForm(scope, scope.$index, 'constraint')"-->
<!-- >-->
<!-- 编辑-->
<!-- </Button>-->
<!-- <Divider type="vertical" />-->
<!-- <Button-->
<!-- type="link"-->
<!-- danger-->
<!-- @click="removeFieldOptionItem(scope, scope.$index, 'constraint')"-->
<!-- >-->
<!-- 移除-->
<!-- </Button>-->
<!-- </template>-->
<!-- </TableColumn>-->
<!-- </Table>-->
<!-- &lt;!&ndash; 表单属性 &ndash;&gt;-->
<!-- <Divider key="property-divider" />-->
<!-- <p class="listener-filed__title" key="property-title">-->
<!-- <span><Icon icon="ep:menu" />字段属性列表</span>-->
<!-- <Button type="primary" @click="openFieldOptionForm(null, -1, 'property')"-->
<!-- >添加属性</Button-->
<!-- >-->
<!-- </p>-->
<!-- <Table :data-source="fieldPropertiesList" key="property-table" :scroll="{ y: 240 }" bordered>-->
<!-- <TableColumn title="序号" width="50px" type="index" />-->
<!-- <TableColumn title="属性编号" dataIndex="id" width="100px" :ellipsis="true" />-->
<!-- <TableColumn title="属性值" dataIndex="value" width="100px" :ellipsis="true" />-->
<!-- <TableColumn title="操作" width="90px">-->
<!-- <template #default="scope">-->
<!-- <Button-->
<!-- type="link"-->
<!-- @click="openFieldOptionForm(scope, scope.$index, 'property')"-->
<!-- >-->
<!-- 编辑-->
<!-- </Button>-->
<!-- <Divider type="vertical" />-->
<!-- <Button-->
<!-- type="link"-->
<!-- danger-->
<!-- @click="removeFieldOptionItem(scope, scope.$index, 'property')"-->
<!-- >-->
<!-- 移除-->
<!-- </Button>-->
<!-- </template>-->
<!-- </TableColumn>-->
<!-- </Table>-->
<!-- &lt;!&ndash; 底部按钮 &ndash;&gt;-->
<!-- <div class="element-drawer__button">-->
<!-- <Button> </Button>-->
<!-- <Button type="primary" @click="saveField"> </Button>-->
<!-- </div>-->
<!-- </Drawer>-->
<!-- <Modal-->
<!-- v-model:open="fieldOptionModelVisible"-->
<!-- :title="optionModelTitle"-->
<!-- width="600px"-->
<!-- destroyOnClose-->
<!-- >-->
<!-- <Form :model="fieldOptionForm" :label-col="{ style: { width: '96px' } }">-->
<!-- <FormItem label="编号/ID" v-if="fieldOptionType !== 'constraint'" key="option-id">-->
<!-- <Input v-model:value="fieldOptionForm.id" allowClear />-->
<!-- </FormItem>-->
<!-- <FormItem label="名称" v-if="fieldOptionType !== 'property'" key="option-name">-->
<!-- <Input v-model:value="fieldOptionForm.name" allowClear />-->
<!-- </FormItem>-->
<!-- <FormItem label="配置" v-if="fieldOptionType === 'constraint'" key="option-config">-->
<!-- <Input v-model:value="fieldOptionForm.config" allowClear />-->
<!-- </FormItem>-->
<!-- <FormItem label="值" v-if="fieldOptionType === 'property'" key="option-value">-->
<!-- <Input v-model:value="fieldOptionForm.value" allowClear />-->
<!-- </FormItem>-->
<!-- </Form>-->
<!-- <template #footer>-->
<!-- <Button @click="fieldOptionModelVisible = false"> </Button>-->
<!-- <Button type="primary" @click="saveFieldOption"> </Button>-->
<!-- </template>-->
<!-- </Modal>-->
</div>
</template>

View File

@ -0,0 +1,7 @@
import MyPropertiesPanel from './PropertiesPanel.vue';
MyPropertiesPanel.install = function (Vue) {
Vue.component(MyPropertiesPanel.name, MyPropertiesPanel);
};
export default MyPropertiesPanel;

View File

@ -0,0 +1,623 @@
<script lang="ts" setup>
import { inject, nextTick, ref, watch } from 'vue';
import { IconifyIcon, PlusOutlined } from '@vben/icons';
import { cloneDeep } from '@vben/utils';
import {
Button,
Divider,
Drawer,
Form,
FormItem,
Input,
Modal,
Select,
SelectOption,
Table,
TableColumn,
} from 'ant-design-vue';
import { createListenerObject, updateElementExtensions } from '../../utils';
import ProcessListenerDialog from './ProcessListenerDialog.vue';
import {
fieldType,
initListenerForm,
initListenerForm2,
initListenerType,
listenerType,
} from './utilSelf';
defineOptions({ name: 'ElementListeners' });
const props = defineProps({
id: {
type: String,
default: '',
},
type: {
type: String,
default: '',
},
});
const prefix = inject('prefix');
const width = inject('width');
const elementListenersList = ref<any[]>([]); //
const listenerForm = ref<any>({}); //
const listenerFormModelVisible = ref(false); //
const fieldsListOfListener = ref<any[]>([]);
const listenerFieldForm = ref<any>({}); //
const listenerFieldFormModelVisible = ref(false); //
const editingListenerIndex = ref(-1); // -1
const editingListenerFieldIndex = ref(-1); // -1
const listenerTypeObject = ref(listenerType);
const fieldTypeObject = ref(fieldType);
const bpmnElement = ref();
const otherExtensionList = ref();
const bpmnElementListeners = ref();
const listenerFormRef = ref();
const listenerFieldFormRef = ref();
const bpmnInstances = () => (window as any)?.bpmnInstances;
const resetListenersList = () => {
bpmnElement.value = bpmnInstances().bpmnElement;
otherExtensionList.value = [];
bpmnElementListeners.value =
bpmnElement.value.businessObject?.extensionElements?.values?.filter(
(ex: any) => ex.$type === `${prefix}:ExecutionListener`,
) ?? [];
elementListenersList.value = bpmnElementListeners.value.map((listener: any) =>
initListenerType(listener),
);
};
//
const openListenerForm = (listener: any, index: number) => {
// debugger
if (listener) {
listenerForm.value = initListenerForm(listener);
editingListenerIndex.value = index;
} else {
listenerForm.value = {};
editingListenerIndex.value = -1; //
}
if (listener && listener.fields) {
fieldsListOfListener.value = listener.fields.map((field: any) => ({
...field,
fieldType: field.string ? 'string' : 'expression',
}));
} else {
fieldsListOfListener.value = [];
listenerForm.value.fields = [];
}
//
listenerFormModelVisible.value = true;
nextTick(() => {
if (listenerFormRef.value) {
listenerFormRef.value.clearValidate();
}
});
};
//
const openListenerFieldForm = (field: any, index: number) => {
listenerFieldForm.value = field ? cloneDeep(field) : {};
editingListenerFieldIndex.value = field ? index : -1;
listenerFieldFormModelVisible.value = true;
nextTick(() => {
if (listenerFieldFormRef.value) {
listenerFieldFormRef.value.clearValidate();
}
});
};
//
const saveListenerFiled = async () => {
// debugger
const validateStatus = await listenerFieldFormRef.value.validate();
if (!validateStatus) return; //
if (editingListenerFieldIndex.value === -1) {
fieldsListOfListener.value.push(listenerFieldForm.value);
listenerForm.value.fields.push(listenerFieldForm.value);
} else {
fieldsListOfListener.value.splice(
editingListenerFieldIndex.value,
1,
listenerFieldForm.value,
);
listenerForm.value.fields.splice(
editingListenerFieldIndex.value,
1,
listenerFieldForm.value,
);
}
listenerFieldFormModelVisible.value = false;
nextTick(() => {
listenerFieldForm.value = {};
});
};
//
const removeListenerField = (index: number) => {
// debugger
Modal.confirm({
title: '确认移除该字段吗?',
content: '此操作不可撤销',
okText: '确 认',
cancelText: '取 消',
onOk() {
fieldsListOfListener.value.splice(index, 1);
listenerForm.value.fields.splice(index, 1);
},
onCancel() {
console.warn('操作取消');
},
});
};
//
const removeListener = (index: number) => {
Modal.confirm({
title: '确认移除该监听器吗?',
content: '此操作不可撤销',
okText: '确 认',
cancelText: '取 消',
onOk() {
bpmnElementListeners.value.splice(index, 1);
elementListenersList.value.splice(index, 1);
updateElementExtensions(bpmnElement.value, [
...otherExtensionList.value,
...bpmnElementListeners.value,
]);
},
onCancel() {
console.warn('操作取消');
},
});
};
//
const saveListenerConfig = async () => {
// debugger
const validateStatus = await listenerFormRef.value.validate();
if (!validateStatus) return; //
const listenerObject = createListenerObject(
listenerForm.value,
false,
prefix,
);
if (editingListenerIndex.value === -1) {
bpmnElementListeners.value.push(listenerObject);
elementListenersList.value.push(listenerForm.value);
} else {
bpmnElementListeners.value.splice(
editingListenerIndex.value,
1,
listenerObject,
);
elementListenersList.value.splice(
editingListenerIndex.value,
1,
listenerForm.value,
);
}
//
otherExtensionList.value =
bpmnElement.value.businessObject?.extensionElements?.values?.filter(
(ex: any) => ex.$type !== `${prefix}:ExecutionListener`,
) ?? [];
updateElementExtensions(bpmnElement.value, [
...otherExtensionList.value,
...bpmnElementListeners.value,
]);
// 4.
listenerFormModelVisible.value = false;
listenerForm.value = {};
};
//
const processListenerDialogRef = ref();
const openProcessListenerDialog = async () => {
processListenerDialogRef.value.open('execution');
};
const selectProcessListener = (listener: any) => {
const listenerForm = initListenerForm2(listener);
const listenerObject = createListenerObject(listenerForm, false, prefix);
bpmnElementListeners.value.push(listenerObject);
elementListenersList.value.push(listenerForm);
//
otherExtensionList.value =
bpmnElement.value.businessObject?.extensionElements?.values?.filter(
(ex: any) => ex.$type !== `${prefix}:ExecutionListener`,
) ?? [];
updateElementExtensions(bpmnElement.value, [
...otherExtensionList.value,
...bpmnElementListeners.value,
]);
};
watch(
() => props.id,
(val: string) => {
if (val && val.length > 0) {
nextTick(() => {
resetListenersList();
});
}
},
{ immediate: true },
);
</script>
<template>
<div class="panel-tab__content">
<Table
:data-source="elementListenersList"
size="small"
bordered
:pagination="false"
>
<TableColumn title="序号" width="50px">
<template #default="{ index }">
{{ index + 1 }}
</template>
</TableColumn>
<TableColumn title="事件类型" width="100px" data-index="event" />
<TableColumn
title="监听器类型"
width="100px"
:custom-render="
({ record }: any) =>
listenerTypeObject[record.listenerType as keyof typeof listenerType]
"
/>
<TableColumn title="操作" width="100px">
<template #default="{ record, index }">
<Button
size="small"
type="link"
@click="openListenerForm(record, index)"
>
编辑
</Button>
<Divider type="vertical" />
<Button
size="small"
type="link"
danger
@click="removeListener(index)"
>
移除
</Button>
</template>
</TableColumn>
</Table>
<div class="element-drawer__button">
<Button type="primary" size="small" @click="openListenerForm(null, -1)">
<template #icon>
<PlusOutlined />
</template>
添加监听器
</Button>
<Button size="small" @click="openProcessListenerDialog">
<template #icon>
<IconifyIcon icon="ep:select" />
</template>
选择监听器
</Button>
</div>
<!-- 监听器 编辑/创建 部分 -->
<Drawer
v-model:open="listenerFormModelVisible"
title="执行监听器"
:width="width as any"
:destroy-on-close="true"
>
<Form
:model="listenerForm"
ref="listenerFormRef"
:label-col="{ span: 6 }"
:wrapper-col="{ span: 18 }"
>
<FormItem
label="事件类型"
name="event"
:rules="[
{
required: true,
message: '请选择事件类型',
trigger: ['blur', 'change'],
},
]"
>
<Select v-model:value="listenerForm.event">
<SelectOption value="start">start</SelectOption>
<SelectOption value="end">end</SelectOption>
</Select>
</FormItem>
<FormItem
label="监听器类型"
name="listenerType"
:rules="[
{
required: true,
message: '请选择监听器类型',
trigger: ['blur', 'change'],
},
]"
>
<Select v-model:value="listenerForm.listenerType">
<SelectOption
v-for="i in Object.keys(listenerTypeObject)"
:key="i"
:value="i"
>
{{ listenerTypeObject[i as keyof typeof listenerType] }}
</SelectOption>
</Select>
</FormItem>
<FormItem
v-if="listenerForm.listenerType === 'classListener'"
label="Java类"
name="class"
key="listener-class"
:rules="[
{
required: true,
message: '请填写Java类',
trigger: ['blur', 'change'],
},
]"
>
<Input v-model:value="listenerForm.class" allow-clear />
</FormItem>
<FormItem
v-if="listenerForm.listenerType === 'expressionListener'"
label="表达式"
name="expression"
key="listener-expression"
:rules="[
{
required: true,
message: '请填写表达式',
trigger: ['blur', 'change'],
},
]"
>
<Input v-model:value="listenerForm.expression" allow-clear />
</FormItem>
<FormItem
v-if="listenerForm.listenerType === 'delegateExpressionListener'"
label="代理表达式"
name="delegateExpression"
key="listener-delegate"
:rules="[
{
required: true,
message: '请填写代理表达式',
trigger: ['blur', 'change'],
},
]"
>
<Input v-model:value="listenerForm.delegateExpression" allow-clear />
</FormItem>
<template v-if="listenerForm.listenerType === 'scriptListener'">
<FormItem
label="脚本格式"
name="scriptFormat"
key="listener-script-format"
:rules="[
{
required: true,
trigger: ['blur', 'change'],
message: '请填写脚本格式',
},
]"
>
<Input v-model:value="listenerForm.scriptFormat" allow-clear />
</FormItem>
<FormItem
label="脚本类型"
name="scriptType"
key="listener-script-type"
:rules="[
{
required: true,
trigger: ['blur', 'change'],
message: '请选择脚本类型',
},
]"
>
<Select v-model:value="listenerForm.scriptType">
<SelectOption value="inlineScript">内联脚本</SelectOption>
<SelectOption value="externalScript">外部脚本</SelectOption>
</Select>
</FormItem>
<FormItem
v-if="listenerForm.scriptType === 'inlineScript'"
label="脚本内容"
name="value"
key="listener-script"
:rules="[
{
required: true,
trigger: ['blur', 'change'],
message: '请填写脚本内容',
},
]"
>
<Input v-model:value="listenerForm.value" allow-clear />
</FormItem>
<FormItem
v-if="listenerForm.scriptType === 'externalScript'"
label="资源地址"
name="resource"
key="listener-resource"
:rules="[
{
required: true,
trigger: ['blur', 'change'],
message: '请填写资源地址',
},
]"
>
<Input v-model:value="listenerForm.resource" allow-clear />
</FormItem>
</template>
</Form>
<Divider />
<p class="listener-filed__title">
<span><IconifyIcon icon="ep:menu" />注入字段</span>
<Button type="primary" @click="openListenerFieldForm(null, -1)">
添加字段
</Button>
</p>
<Table
:data-source="fieldsListOfListener"
size="small"
:scroll="{ y: 240 }"
:pagination="false"
bordered
style="flex: none"
>
<TableColumn title="序号" width="50px">
<template #default="{ index }">
{{ index + 1 }}
</template>
</TableColumn>
<TableColumn title="字段名称" width="100px" data-index="name" />
<TableColumn
title="字段类型"
width="80px"
:custom-render="
({ record }: any) =>
fieldTypeObject[record.fieldType as keyof typeof fieldType]
"
/>
<TableColumn
title="字段值/表达式"
width="100px"
:custom-render="
({ record }: any) => record.string || record.expression
"
/>
<TableColumn title="操作" width="130px">
<template #default="{ record, index }">
<Button
size="small"
type="link"
@click="openListenerFieldForm(record, index)"
>
编辑
</Button>
<Divider type="vertical" />
<Button
size="small"
type="link"
danger
@click="removeListenerField(index)"
>
移除
</Button>
</template>
</TableColumn>
</Table>
<div class="element-drawer__button">
<Button @click="listenerFormModelVisible = false"> </Button>
<Button type="primary" @click="saveListenerConfig"> </Button>
</div>
</Drawer>
<!-- 注入字段 编辑/创建 部分 -->
<Modal
title="字段配置"
v-model:open="listenerFieldFormModelVisible"
width="600px"
:destroy-on-close="true"
>
<Form
:model="listenerFieldForm"
ref="listenerFieldFormRef"
:label-col="{ span: 6 }"
:wrapper-col="{ span: 18 }"
style="height: 136px"
>
<FormItem
label="字段名称:"
name="name"
:rules="[
{
required: true,
message: '请填写字段名称',
trigger: ['blur', 'change'],
},
]"
>
<Input v-model:value="listenerFieldForm.name" allow-clear />
</FormItem>
<FormItem
label="字段类型:"
name="fieldType"
:rules="[
{
required: true,
message: '请选择字段类型',
trigger: ['blur', 'change'],
},
]"
>
<Select v-model:value="listenerFieldForm.fieldType">
<SelectOption
v-for="i in Object.keys(fieldTypeObject)"
:key="i"
:value="i"
>
{{ fieldTypeObject[i as keyof typeof fieldType] }}
</SelectOption>
</Select>
</FormItem>
<FormItem
v-if="listenerFieldForm.fieldType === 'string'"
label="字段值:"
name="string"
key="field-string"
:rules="[
{
required: true,
message: '请填写字段值',
trigger: ['blur', 'change'],
},
]"
>
<Input v-model:value="listenerFieldForm.string" allow-clear />
</FormItem>
<FormItem
v-if="listenerFieldForm.fieldType === 'expression'"
label="表达式:"
name="expression"
key="field-expression"
:rules="[
{
required: true,
message: '请填写表达式',
trigger: ['blur', 'change'],
},
]"
>
<Input v-model:value="listenerFieldForm.expression" allow-clear />
</FormItem>
</Form>
<template #footer>
<Button size="small" @click="listenerFieldFormModelVisible = false">
</Button>
<Button size="small" type="primary" @click="saveListenerFiled">
</Button>
</template>
</Modal>
</div>
<!-- 选择弹窗 -->
<ProcessListenerDialog
ref="processListenerDialogRef"
@select="selectProcessListener"
/>
</template>

View File

@ -0,0 +1,110 @@
<!-- 执行器选择 -->
<script setup lang="ts">
import type { BpmProcessListenerApi } from '#/api/bpm/processListener';
import { reactive, ref } from 'vue';
import { CommonStatusEnum, DICT_TYPE } from '@vben/constants';
import { Button, Modal, Pagination, Table } from 'ant-design-vue';
import { getProcessListenerPage } from '#/api/bpm/processListener';
import { ContentWrap } from '#/components/content-wrap';
import { DictTag } from '#/components/dict-tag';
/** BPM 流程 表单 */
defineOptions({ name: 'ProcessListenerDialog' });
/** 提交表单 */
const emit = defineEmits(['success', 'select']);
const dialogVisible = ref(false); //
const loading = ref(true); //
const list = ref<BpmProcessListenerApi.ProcessListener[]>([]); //
const total = ref(0); //
const queryParams = reactive({
pageNo: 1,
pageSize: 10,
type: '',
status: CommonStatusEnum.ENABLE,
});
/** 打开弹窗 */
const open = async (type: string) => {
queryParams.pageNo = 1;
queryParams.type = type;
await getList();
dialogVisible.value = true;
};
defineExpose({ open }); // open
/** 查询列表 */
const getList = async () => {
loading.value = true;
try {
const data = await getProcessListenerPage(queryParams);
list.value = data.list;
total.value = data.total;
} finally {
loading.value = false;
}
};
// success
const select = async (row: BpmProcessListenerApi.ProcessListener) => {
dialogVisible.value = false;
//
emit('select', row);
};
</script>
<template>
<Modal
title="请选择监听器"
v-model:open="dialogVisible"
width="1024px"
:footer="null"
>
<ContentWrap>
<Table
:loading="loading"
:data-source="list"
:pagination="false"
:scroll="{ x: 'max-content' }"
>
<Table.Column title="名字" align="center" data-index="name" />
<Table.Column title="类型" align="center" data-index="type">
<template #default="{ record }">
<DictTag
:type="DICT_TYPE.BPM_PROCESS_LISTENER_TYPE"
:value="record.type"
/>
</template>
</Table.Column>
<Table.Column title="事件" align="center" data-index="event" />
<Table.Column title="值类型" align="center" data-index="valueType">
<template #default="{ record }">
<DictTag
:type="DICT_TYPE.BPM_PROCESS_LISTENER_VALUE_TYPE"
:value="record.valueType"
/>
</template>
</Table.Column>
<Table.Column title="值" align="center" data-index="value" />
<Table.Column title="操作" align="center">
<template #default="{ record }">
<Button type="primary" @click="select(record)"> </Button>
</template>
</Table.Column>
</Table>
<!-- 分页 -->
<div class="mt-4 flex justify-end">
<Pagination
:total="total"
v-model:current="queryParams.pageNo"
v-model:page-size="queryParams.pageSize"
show-size-changer
@change="getList"
/>
</div>
</ContentWrap>
</Modal>
</template>

View File

@ -0,0 +1,600 @@
<script lang="ts" setup>
import { inject, nextTick, ref, watch } from 'vue';
import { MenuOutlined, PlusOutlined, SelectOutlined } from '@vben/icons';
import { cloneDeep } from '@vben/utils';
import {
Button,
Divider,
Drawer,
Form,
FormItem,
Input,
Modal,
Select,
SelectOption,
Table,
TableColumn,
} from 'ant-design-vue';
import ProcessListenerDialog from '#/components/bpmn-process-designer/package/penal/listeners/ProcessListenerDialog.vue';
import { createListenerObject, updateElementExtensions } from '../../utils';
import {
eventType,
fieldType,
initListenerForm,
initListenerForm2,
initListenerType,
listenerType,
} from './utilSelf';
defineOptions({ name: 'UserTaskListeners' });
const props = defineProps<Props>();
interface Props {
id?: string;
type?: string;
}
const prefix = inject<string>('prefix');
const width = inject<number>('width');
const elementListenersList = ref<any[]>([]);
const listenerEventTypeObject = ref(eventType);
const listenerTypeObject = ref(listenerType);
const listenerFormModelVisible = ref(false);
const listenerForm = ref<any>({});
const fieldTypeObject = ref(fieldType);
const fieldsListOfListener = ref<any[]>([]);
const listenerFieldFormModelVisible = ref(false); //
const editingListenerIndex = ref(-1); // -1
const editingListenerFieldIndex = ref<any>(-1); // -1
const listenerFieldForm = ref<any>({}); //
const bpmnElement = ref<any>();
const bpmnElementListeners = ref<any[]>([]);
const otherExtensionList = ref<any[]>([]);
const listenerFormRef = ref<any>({});
const listenerFieldFormRef = ref<any>({});
interface BpmnInstances {
bpmnElement: any;
[key: string]: any;
}
declare global {
interface Window {
bpmnInstances?: BpmnInstances;
}
}
const bpmnInstances = () => window.bpmnInstances;
const resetListenersList = () => {
// console.log(
// bpmnInstances().bpmnElement,
// 'window.bpmnInstances.bpmnElementwindow.bpmnInstances.bpmnElementwindow.bpmnInstances.bpmnElementwindow.bpmnInstances.bpmnElementwindow.bpmnInstances.bpmnElementwindow.bpmnInstances.bpmnElement',
// );
bpmnElement.value = bpmnInstances()?.bpmnElement;
otherExtensionList.value = [];
bpmnElementListeners.value =
bpmnElement.value.businessObject?.extensionElements?.values.filter(
(ex: any) => ex.$type === `${prefix}:TaskListener`,
) ?? [];
elementListenersList.value = bpmnElementListeners.value.map((listener) =>
initListenerType(listener),
);
};
const openListenerForm = (listener: any, index?: number) => {
if (listener) {
listenerForm.value = initListenerForm(listener);
editingListenerIndex.value = index || -1;
} else {
listenerForm.value = {};
editingListenerIndex.value = -1; //
}
if (listener && listener.fields) {
fieldsListOfListener.value = listener.fields.map((field: any) => ({
...field,
fieldType: field.string ? 'string' : 'expression',
}));
} else {
fieldsListOfListener.value = [];
listenerForm.value.fields = [];
}
//
listenerFormModelVisible.value = true;
nextTick(() => {
if (listenerFormRef.value) listenerFormRef.value.clearValidate();
});
};
//
const removeListener = (_: any, index: number) => {
// console.log(listener, 'listener');
Modal.confirm({
title: '提示',
content: '确认移除该监听器吗?',
okText: '确 认',
cancelText: '取 消',
onOk() {
bpmnElementListeners.value.splice(index, 1);
elementListenersList.value.splice(index, 1);
updateElementExtensions(bpmnElement.value, [
...otherExtensionList.value,
...bpmnElementListeners.value,
]);
},
onCancel() {
// console.info('');
},
});
};
//
const saveListenerConfig = async () => {
const validateStatus = await listenerFormRef.value.validate();
if (!validateStatus) return; //
const listenerObject = createListenerObject(listenerForm.value, true, prefix);
if (editingListenerIndex.value === -1) {
bpmnElementListeners.value.push(listenerObject);
elementListenersList.value.push(listenerForm.value);
} else {
bpmnElementListeners.value.splice(
editingListenerIndex.value,
1,
listenerObject,
);
elementListenersList.value.splice(
editingListenerIndex.value,
1,
listenerForm.value,
);
}
//
otherExtensionList.value =
bpmnElement.value.businessObject?.extensionElements?.values?.filter(
(ex: any) => ex.$type !== `${prefix}:TaskListener`,
) ?? [];
updateElementExtensions(bpmnElement.value, [
...otherExtensionList.value,
...bpmnElementListeners.value,
]);
// 4.
listenerFormModelVisible.value = false;
listenerForm.value = {};
};
//
const openListenerFieldForm = (field: any, index?: number) => {
listenerFieldForm.value = field ? cloneDeep(field) : {};
editingListenerFieldIndex.value = field ? index : -1;
listenerFieldFormModelVisible.value = true;
nextTick(() => {
if (listenerFieldFormRef.value) listenerFieldFormRef.value.clearValidate();
});
};
//
const saveListenerFiled = async () => {
const validateStatus = await listenerFieldFormRef.value.validate();
if (!validateStatus) return; //
if (editingListenerFieldIndex.value === -1) {
fieldsListOfListener.value.push(listenerFieldForm.value);
listenerForm.value.fields.push(listenerFieldForm.value);
} else {
fieldsListOfListener.value.splice(
editingListenerFieldIndex.value,
1,
listenerFieldForm.value,
);
listenerForm.value.fields.splice(
editingListenerFieldIndex.value,
1,
listenerFieldForm.value,
);
}
listenerFieldFormModelVisible.value = false;
nextTick(() => {
listenerFieldForm.value = {};
});
};
//
const removeListenerField = (_: any, index: number) => {
// console.log(field, 'field');
Modal.confirm({
title: '提示',
content: '确认移除该字段吗?',
okText: '确 认',
cancelText: '取 消',
onOk() {
fieldsListOfListener.value.splice(index, 1);
listenerForm.value.fields.splice(index, 1);
},
onCancel() {
// console.info('');
},
});
};
//
const processListenerDialogRef = ref<any>();
const openProcessListenerDialog = async () => {
processListenerDialogRef.value.open('task');
};
const selectProcessListener = (listener: any) => {
const listenerForm = initListenerForm2(listener);
const listenerObject = createListenerObject(listenerForm, true, prefix);
bpmnElementListeners.value.push(listenerObject);
elementListenersList.value.push(listenerForm);
//
otherExtensionList.value =
bpmnElement.value.businessObject?.extensionElements?.values?.filter(
(ex: any) => ex.$type !== `${prefix}:TaskListener`,
) ?? [];
updateElementExtensions(
bpmnElement.value,
otherExtensionList.value?.concat(bpmnElementListeners.value),
);
};
watch(
() => props.id,
(val) => {
val &&
val.length > 0 &&
nextTick(() => {
resetListenersList();
});
},
{ immediate: true },
);
</script>
<template>
<div class="panel-tab__content">
<Table :data="elementListenersList" size="small" bordered>
<TableColumn title="序号" width="50px" type="index" />
<TableColumn
title="事件类型"
width="80px"
:ellipsis="{ showTitle: true }"
:custom-render="
({ record }: any) =>
listenerEventTypeObject[record.event as keyof typeof eventType]
"
/>
<TableColumn
title="事件id"
width="80px"
data-index="id"
:ellipsis="{ showTitle: true }"
/>
<TableColumn
title="监听器类型"
width="80px"
:ellipsis="{ showTitle: true }"
:custom-render="
({ record }: any) =>
listenerTypeObject[record.listenerType as keyof typeof listenerType]
"
/>
<TableColumn title="操作" width="90px">
<template #default="{ record, index }">
<Button
size="small"
type="link"
@click="openListenerForm(record, index)"
>
编辑
</Button>
<Divider type="vertical" />
<Button
size="small"
type="link"
danger
@click="removeListener(record, index)"
>
移除
</Button>
</template>
</TableColumn>
</Table>
<div class="element-drawer__button">
<Button size="small" type="primary" @click="openListenerForm(null)">
<template #icon><PlusOutlined /></template>
添加监听器
</Button>
<Button size="small" @click="openProcessListenerDialog">
<template #icon><SelectOutlined /></template>
选择监听器
</Button>
</div>
<!-- 监听器 编辑/创建 部分 -->
<Drawer
v-model:open="listenerFormModelVisible"
title="任务监听器"
:width="width"
:destroy-on-close="true"
>
<Form
:model="listenerForm"
:label-col="{ span: 8 }"
:wrapper-col="{ span: 16 }"
ref="listenerFormRef"
>
<FormItem
label="事件类型"
name="event"
:rules="[{ required: true, message: '请选择事件类型' }]"
>
<Select v-model:value="listenerForm.event">
<SelectOption
v-for="i in Object.keys(listenerEventTypeObject)"
:key="i"
:value="i"
>
{{ listenerEventTypeObject[i as keyof typeof eventType] }}
</SelectOption>
</Select>
</FormItem>
<FormItem
label="监听器ID"
name="id"
:rules="[{ required: true, message: '请输入监听器ID' }]"
>
<Input v-model:value="listenerForm.id" allow-clear />
</FormItem>
<FormItem
label="监听器类型"
name="listenerType"
:rules="[{ required: true, message: '请选择监听器类型' }]"
>
<Select v-model:value="listenerForm.listenerType">
<SelectOption
v-for="i in Object.keys(listenerTypeObject)"
:key="i"
:value="i"
>
{{ listenerTypeObject[i as keyof typeof listenerType] }}
</SelectOption>
</Select>
</FormItem>
<FormItem
v-if="listenerForm.listenerType === 'classListener'"
label="Java类"
name="class"
key="listener-class"
:rules="[{ required: true, message: '请输入Java类' }]"
>
<Input v-model:value="listenerForm.class" allow-clear />
</FormItem>
<FormItem
v-if="listenerForm.listenerType === 'expressionListener'"
label="表达式"
name="expression"
key="listener-expression"
:rules="[{ required: true, message: '请输入表达式' }]"
>
<Input v-model:value="listenerForm.expression" allow-clear />
</FormItem>
<FormItem
v-if="listenerForm.listenerType === 'delegateExpressionListener'"
label="代理表达式"
name="delegateExpression"
key="listener-delegate"
:rules="[{ required: true, message: '请输入代理表达式' }]"
>
<Input v-model:value="listenerForm.delegateExpression" allow-clear />
</FormItem>
<template v-if="listenerForm.listenerType === 'scriptListener'">
<FormItem
label="脚本格式"
name="scriptFormat"
key="listener-script-format"
:rules="[{ required: true, message: '请填写脚本格式' }]"
>
<Input v-model:value="listenerForm.scriptFormat" allow-clear />
</FormItem>
<FormItem
label="脚本类型"
name="scriptType"
key="listener-script-type"
:rules="[{ required: true, message: '请选择脚本类型' }]"
>
<Select v-model:value="listenerForm.scriptType">
<SelectOption value="inlineScript">内联脚本</SelectOption>
<SelectOption value="externalScript">外部脚本</SelectOption>
</Select>
</FormItem>
<FormItem
v-if="listenerForm.scriptType === 'inlineScript'"
label="脚本内容"
name="value"
key="listener-script"
:rules="[{ required: true, message: '请填写脚本内容' }]"
>
<Input v-model:value="listenerForm.value" allow-clear />
</FormItem>
<FormItem
v-if="listenerForm.scriptType === 'externalScript'"
label="资源地址"
name="resource"
key="listener-resource"
:rules="[{ required: true, message: '请填写资源地址' }]"
>
<Input v-model:value="listenerForm.resource" allow-clear />
</FormItem>
</template>
<template v-if="listenerForm.event === 'timeout'">
<FormItem
label="定时器类型"
name="eventDefinitionType"
key="eventDefinitionType"
>
<Select v-model:value="listenerForm.eventDefinitionType">
<SelectOption value="date">日期</SelectOption>
<SelectOption value="duration">持续时长</SelectOption>
<SelectOption value="cycle">循环</SelectOption>
<SelectOption value="null"></SelectOption>
</Select>
</FormItem>
<FormItem
v-if="
!!listenerForm.eventDefinitionType &&
listenerForm.eventDefinitionType !== 'null'
"
label="定时器"
name="eventTimeDefinitions"
key="eventTimeDefinitions"
:rules="[{ required: true, message: '请填写定时器配置' }]"
>
<Input
v-model:value="listenerForm.eventTimeDefinitions"
allow-clear
/>
</FormItem>
</template>
</Form>
<Divider />
<p class="listener-filed__title">
<span><MenuOutlined />注入字段</span>
<Button
size="small"
type="primary"
@click="openListenerFieldForm(null)"
>
添加字段
</Button>
</p>
<Table
:data="fieldsListOfListener"
size="small"
:scroll="{ y: 240 }"
bordered
style="flex: none"
>
<TableColumn title="序号" width="50px" type="index" />
<TableColumn title="字段名称" width="100px" data-index="name" />
<TableColumn
title="字段类型"
width="80px"
:ellipsis="{ showTitle: true }"
:custom-render="
({ record }: any) =>
fieldTypeObject[record.fieldType as keyof typeof fieldType]
"
/>
<TableColumn
title="字段值/表达式"
width="100px"
:ellipsis="{ showTitle: true }"
:custom-render="
({ record }: any) => record.string || record.expression
"
/>
<TableColumn title="操作" width="100px">
<template #default="{ record, index }">
<Button
size="small"
type="link"
@click="openListenerFieldForm(record, index)"
>
编辑
</Button>
<Divider type="vertical" />
<Button
size="small"
type="link"
danger
@click="removeListenerField(record, index)"
>
移除
</Button>
</template>
</TableColumn>
</Table>
<div class="element-drawer__button">
<Button size="small" @click="listenerFormModelVisible = false">
</Button>
<Button size="small" type="primary" @click="saveListenerConfig">
</Button>
</div>
</Drawer>
<!-- 注入字段 编辑/创建 部分 -->
<Modal
title="字段配置"
v-model:open="listenerFieldFormModelVisible"
:width="600"
:destroy-on-close="true"
>
<Form
:model="listenerFieldForm"
:label-col="{ span: 8 }"
:wrapper-col="{ span: 16 }"
ref="listenerFieldFormRef"
style="height: 136px"
>
<FormItem
label="字段名称:"
name="name"
:rules="[{ required: true, message: '请输入字段名称' }]"
>
<Input v-model:value="listenerFieldForm.name" allow-clear />
</FormItem>
<FormItem
label="字段类型:"
name="fieldType"
:rules="[{ required: true, message: '请选择字段类型' }]"
>
<Select v-model:value="listenerFieldForm.fieldType">
<SelectOption
v-for="i in Object.keys(fieldTypeObject)"
:key="i"
:value="i"
>
{{ fieldTypeObject[i as keyof typeof fieldType] }}
</SelectOption>
</Select>
</FormItem>
<FormItem
v-if="listenerFieldForm.fieldType === 'string'"
label="字段值:"
name="string"
key="field-string"
:rules="[{ required: true, message: '请输入字段值' }]"
>
<Input v-model:value="listenerFieldForm.string" allow-clear />
</FormItem>
<FormItem
v-if="listenerFieldForm.fieldType === 'expression'"
label="表达式:"
name="expression"
key="field-expression"
:rules="[{ required: true, message: '请输入表达式' }]"
>
<Input v-model:value="listenerFieldForm.expression" allow-clear />
</FormItem>
</Form>
<template #footer>
<Button size="small" @click="listenerFieldFormModelVisible = false">
</Button>
<Button size="small" type="primary" @click="saveListenerFiled">
</Button>
</template>
</Modal>
</div>
<!-- 选择弹窗 -->
<ProcessListenerDialog
ref="processListenerDialogRef"
@select="selectProcessListener"
/>
</template>

View File

@ -0,0 +1,98 @@
// 初始化表单数据
import { cloneDeep } from '@vben/utils';
export function initListenerForm(listener: any) {
let self = {
...listener,
};
if (listener.script) {
self = {
...listener,
...listener.script,
scriptType: listener.script.resource ? 'externalScript' : 'inlineScript',
};
}
if (
listener.event === 'timeout' &&
listener.eventDefinitions &&
listener.eventDefinitions.length > 0
) {
let k = '';
for (const key in listener.eventDefinitions[0]) {
// console.log(listener.eventDefinitions, key);
if (key.includes('time')) {
k = key;
self.eventDefinitionType = key.replace('time', '').toLowerCase();
}
}
// console.log(k);
self.eventTimeDefinitions = listener.eventDefinitions[0][k].body;
}
return self;
}
export function initListenerType(listener: any) {
let listenerType;
if (listener.class) listenerType = 'classListener';
if (listener.expression) listenerType = 'expressionListener';
if (listener.delegateExpression) listenerType = 'delegateExpressionListener';
if (listener.script) listenerType = 'scriptListener';
return {
...cloneDeep(listener),
...listener.script,
listenerType,
};
}
/** 将 ProcessListenerDO 转换成 initListenerForm 想同的 Form 对象 */
export function initListenerForm2(processListener: any) {
switch (processListener.valueType) {
case 'class': {
return {
listenerType: 'classListener',
class: processListener.value,
event: processListener.event,
fields: [],
};
}
case 'delegateExpression': {
return {
listenerType: 'delegateExpressionListener',
delegateExpression: processListener.value,
event: processListener.event,
fields: [],
};
}
case 'expression': {
return {
listenerType: 'expressionListener',
expression: processListener.value,
event: processListener.event,
fields: [],
};
}
// No default
}
throw new Error('未知的监听器类型');
}
export const listenerType = {
classListener: 'Java 类',
expressionListener: '表达式',
delegateExpressionListener: '代理表达式',
scriptListener: '脚本',
};
export const eventType = {
create: '创建',
assignment: '指派',
complete: '完成',
delete: '删除',
update: '更新',
timeout: '超时',
};
export const fieldType = {
string: '字符串',
expression: '表达式',
};

View File

@ -0,0 +1,526 @@
<script lang="ts" setup>
import { inject, nextTick, onBeforeUnmount, ref, toRaw, watch } from 'vue';
import {
Button,
Checkbox,
Form,
FormItem,
Input,
InputNumber,
Radio,
RadioGroup,
Select,
} from 'ant-design-vue';
import {
APPROVE_METHODS,
ApproveMethodType,
} from '#/components/simple-process-design/consts';
defineOptions({ name: 'ElementMultiInstance' });
const props = defineProps({
businessObject: {
type: Object,
required: false,
default: () => ({}),
},
type: {
type: String,
required: false,
default: '',
},
id: {
type: String,
required: false,
default: '',
},
});
const prefix = inject<string>('prefix');
const loopCharacteristics = ref('');
//
const defaultLoopInstanceForm = ref({
completionCondition: '',
loopCardinality: '',
extensionElements: [],
asyncAfter: false,
asyncBefore: false,
exclusive: false,
});
interface LoopInstanceForm {
completionCondition?: string;
loopCardinality?: string;
extensionElements?: any[];
asyncAfter?: boolean;
asyncBefore?: boolean;
exclusive?: boolean;
collection?: string;
elementVariable?: string;
timeCycle?: string;
}
const loopInstanceForm = ref<LoopInstanceForm>({});
const bpmnElement = ref<any>(null);
const multiLoopInstance = ref<any>(null);
declare global {
interface Window {
bpmnInstances?: () => any;
}
}
const bpmnInstances = () => (window as any)?.bpmnInstances;
const getElementLoop = (businessObject: any): void => {
if (!businessObject.loopCharacteristics) {
loopCharacteristics.value = 'Null';
loopInstanceForm.value = {};
return;
}
if (
businessObject.loopCharacteristics.$type ===
'bpmn:StandardLoopCharacteristics'
) {
loopCharacteristics.value = 'StandardLoop';
loopInstanceForm.value = {};
return;
}
loopCharacteristics.value = businessObject.loopCharacteristics.isSequential
? 'SequentialMultiInstance'
: 'ParallelMultiInstance';
//
loopInstanceForm.value = {
...defaultLoopInstanceForm.value,
...businessObject.loopCharacteristics,
completionCondition:
businessObject.loopCharacteristics?.completionCondition?.body ?? '',
loopCardinality:
businessObject.loopCharacteristics?.loopCardinality?.body ?? '',
};
// businessObject loopCharacteristics
multiLoopInstance.value =
bpmnInstances().bpmnElement.businessObject.loopCharacteristics;
//
if (
businessObject.loopCharacteristics.extensionElements &&
businessObject.loopCharacteristics.extensionElements.values &&
businessObject.loopCharacteristics.extensionElements.values.length > 0
) {
loopInstanceForm.value.timeCycle =
businessObject.loopCharacteristics.extensionElements.values[0].body;
}
};
const changeLoopCharacteristicsType = (type: any): void => {
// this.loopInstanceForm = { ...this.defaultLoopInstanceForm }; //
//
if (type === 'Null') {
bpmnInstances().modeling.updateProperties(toRaw(bpmnElement.value), {
loopCharacteristics: null,
});
return;
}
//
if (type === 'StandardLoop') {
const loopCharacteristicsObject = bpmnInstances().moddle.create(
'bpmn:StandardLoopCharacteristics',
);
bpmnInstances().modeling.updateProperties(toRaw(bpmnElement.value), {
loopCharacteristics: loopCharacteristicsObject,
});
multiLoopInstance.value = null;
return;
}
//
multiLoopInstance.value =
type === 'SequentialMultiInstance'
? bpmnInstances().moddle.create('bpmn:MultiInstanceLoopCharacteristics', {
isSequential: true,
})
: bpmnInstances().moddle.create('bpmn:MultiInstanceLoopCharacteristics', {
// eslint-disable-next-line no-template-curly-in-string
collection: '${coll_userList}',
});
bpmnInstances().modeling.updateProperties(toRaw(bpmnElement.value), {
loopCharacteristics: toRaw(multiLoopInstance.value),
});
};
//
const updateLoopCardinality = (cardinality: string): void => {
let loopCardinality = null;
if (cardinality && cardinality.length > 0) {
loopCardinality = bpmnInstances().moddle.create('bpmn:FormalExpression', {
body: cardinality,
});
}
bpmnInstances().modeling.updateModdleProperties(
toRaw(bpmnElement.value),
multiLoopInstance.value,
{
loopCardinality,
},
);
};
//
const updateLoopCondition = (condition: string): void => {
let completionCondition = null;
if (condition && condition.length > 0) {
completionCondition = bpmnInstances().moddle.create(
'bpmn:FormalExpression',
{
body: condition,
},
);
}
bpmnInstances().modeling.updateModdleProperties(
toRaw(bpmnElement.value),
multiLoopInstance.value,
{
completionCondition,
},
);
};
//
const updateLoopTimeCycle = (timeCycle: string): void => {
const extensionElements = bpmnInstances().moddle.create(
'bpmn:ExtensionElements',
{
values: [
bpmnInstances().moddle.create(`${prefix}:FailedJobRetryTimeCycle`, {
body: timeCycle,
}),
],
},
);
bpmnInstances().modeling.updateModdleProperties(
toRaw(bpmnElement.value),
multiLoopInstance.value,
{
extensionElements,
},
);
};
//
const updateLoopBase = (): void => {
bpmnInstances().modeling.updateModdleProperties(
toRaw(bpmnElement.value),
multiLoopInstance.value,
{
collection: loopInstanceForm.value.collection || null,
elementVariable: loopInstanceForm.value.elementVariable || null,
},
);
};
//
const updateLoopAsync = (key: any): void => {
const { asyncBefore, asyncAfter } = loopInstanceForm.value;
let asyncAttr = Object.create(null);
if (!asyncBefore && !asyncAfter) {
// this.$set(this.loopInstanceForm, "exclusive", false);
loopInstanceForm.value.exclusive = false;
asyncAttr = {
asyncBefore: false,
asyncAfter: false,
exclusive: false,
extensionElements: null,
};
} else {
// @ts-ignore
asyncAttr[key] = loopInstanceForm.value[key];
}
bpmnInstances().modeling.updateModdleProperties(
toRaw(bpmnElement.value),
multiLoopInstance.value,
asyncAttr,
);
};
const changeConfig = (config: string): void => {
switch (config) {
case '会签': {
changeLoopCharacteristicsType('ParallelMultiInstance');
// eslint-disable-next-line no-template-curly-in-string
updateLoopCondition('${ nrOfCompletedInstances >= nrOfInstances }');
break;
}
case '依次审批': {
changeLoopCharacteristicsType('SequentialMultiInstance');
updateLoopCardinality('1');
// eslint-disable-next-line no-template-curly-in-string
updateLoopCondition('${ nrOfCompletedInstances >= nrOfInstances }');
break;
}
case '或签': {
changeLoopCharacteristicsType('ParallelMultiInstance');
// eslint-disable-next-line no-template-curly-in-string
updateLoopCondition('${ nrOfCompletedInstances > 0 }');
break;
}
// No default
}
};
/**
* -----新版本多实例-----
*/
const approveMethod = ref<ApproveMethodType | undefined>();
const approveRatio = ref<number>(100);
const otherExtensions = ref<any[]>([]);
const getElementLoopNew = (): void => {
if (props.type === 'UserTask') {
const extensionElements =
bpmnElement.value.businessObject?.extensionElements ??
bpmnInstances().moddle.create('bpmn:ExtensionElements', { values: [] });
approveMethod.value = extensionElements.values.find(
(ex: any) => ex.$type === `${prefix}:ApproveMethod`,
)?.value;
otherExtensions.value =
extensionElements.values.filter(
(ex: any) => ex.$type !== `${prefix}:ApproveMethod`,
) ?? [];
if (!approveMethod.value) {
approveMethod.value = ApproveMethodType.SEQUENTIAL_APPROVE;
updateLoopCharacteristics();
}
}
};
const onApproveMethodChange = (): void => {
approveRatio.value = 100;
updateLoopCharacteristics();
};
const onApproveRatioChange = (): void => {
updateLoopCharacteristics();
};
const updateLoopCharacteristics = (): void => {
// ApproveMethodmultiInstanceLoopCharacteristics
if (approveMethod.value === ApproveMethodType.RANDOM_SELECT_ONE_APPROVE) {
bpmnInstances().modeling.updateProperties(toRaw(bpmnElement.value), {
loopCharacteristics: null,
});
} else {
if (approveMethod.value === ApproveMethodType.APPROVE_BY_RATIO) {
multiLoopInstance.value = bpmnInstances().moddle.create(
'bpmn:MultiInstanceLoopCharacteristics',
// eslint-disable-next-line no-template-curly-in-string
{ isSequential: false, collection: '${coll_userList}' },
);
multiLoopInstance.value.completionCondition =
bpmnInstances().moddle.create('bpmn:FormalExpression', {
body: `\${ nrOfCompletedInstances/nrOfInstances >= ${
approveRatio.value / 100
}}`,
});
}
if (approveMethod.value === ApproveMethodType.ANY_APPROVE) {
multiLoopInstance.value = bpmnInstances().moddle.create(
'bpmn:MultiInstanceLoopCharacteristics',
// eslint-disable-next-line no-template-curly-in-string
{ isSequential: false, collection: '${coll_userList}' },
);
multiLoopInstance.value.completionCondition =
bpmnInstances().moddle.create('bpmn:FormalExpression', {
// eslint-disable-next-line no-template-curly-in-string
body: '${ nrOfCompletedInstances > 0 }',
});
}
if (approveMethod.value === ApproveMethodType.SEQUENTIAL_APPROVE) {
multiLoopInstance.value = bpmnInstances().moddle.create(
'bpmn:MultiInstanceLoopCharacteristics',
// eslint-disable-next-line no-template-curly-in-string
{ isSequential: true, collection: '${coll_userList}' },
);
multiLoopInstance.value.loopCardinality = bpmnInstances().moddle.create(
'bpmn:FormalExpression',
{
body: '1',
},
);
multiLoopInstance.value.completionCondition =
bpmnInstances().moddle.create('bpmn:FormalExpression', {
// eslint-disable-next-line no-template-curly-in-string
body: '${ nrOfCompletedInstances >= nrOfInstances }',
});
}
bpmnInstances().modeling.updateProperties(toRaw(bpmnElement.value), {
loopCharacteristics: toRaw(multiLoopInstance.value),
});
}
// ApproveMethodExtensionElements
const extensions = bpmnInstances().moddle.create('bpmn:ExtensionElements', {
values: [
...otherExtensions.value,
bpmnInstances().moddle.create(`${prefix}:ApproveMethod`, {
value: approveMethod.value,
}),
],
});
bpmnInstances().modeling.updateProperties(toRaw(bpmnElement.value), {
extensionElements: extensions,
});
};
onBeforeUnmount(() => {
multiLoopInstance.value = null;
bpmnElement.value = null;
});
watch(
() => props.id,
(val) => {
if (val) {
nextTick(() => {
bpmnElement.value = bpmnInstances().bpmnElement;
// getElementLoop(val)
getElementLoopNew();
});
}
},
{ immediate: true },
);
</script>
<template>
<div class="panel-tab__content">
<RadioGroup
v-if="type === 'UserTask'"
v-model:value="approveMethod"
@change="onApproveMethodChange"
>
<div class="flex-col">
<div v-for="(item, index) in APPROVE_METHODS" :key="index">
<Radio :value="item.value">
{{ item.label }}
</Radio>
<FormItem prop="approveRatio">
<InputNumber
v-model:value="approveRatio"
:min="10"
:max="100"
:step="10"
size="small"
v-if="
item.value === ApproveMethodType.APPROVE_BY_RATIO &&
approveMethod === ApproveMethodType.APPROVE_BY_RATIO
"
@change="onApproveRatioChange"
/>
</FormItem>
</div>
</div>
</RadioGroup>
<div v-else>UserTask</div>
<!-- 与Simple设计器配置合并保留以前的代码 -->
<Form :label-col="{ span: 6 }" style="display: none">
<FormItem label="快捷配置">
<Button size="small" @click="() => changeConfig('依次审批')">
依次审批
</Button>
<Button size="small" @click="() => changeConfig('会签')">会签</Button>
<Button size="small" @click="() => changeConfig('或签')">或签</Button>
</FormItem>
<FormItem label="会签类型">
<Select
v-model:value="loopCharacteristics"
@change="changeLoopCharacteristicsType"
>
<Select.Option value="ParallelMultiInstance">
并行多重事件
</Select.Option>
<Select.Option value="SequentialMultiInstance">
时序多重事件
</Select.Option>
<Select.Option value="Null"></Select.Option>
</Select>
</FormItem>
<template
v-if="
loopCharacteristics === 'ParallelMultiInstance' ||
loopCharacteristics === 'SequentialMultiInstance'
"
>
<FormItem label="循环数量" key="loopCardinality">
<Input
v-model:value="loopInstanceForm.loopCardinality"
allow-clear
@change="
() =>
updateLoopCardinality(loopInstanceForm.loopCardinality || '')
"
/>
</FormItem>
<FormItem label="集合" key="collection" v-show="false">
<Input
v-model:value="loopInstanceForm.collection"
allow-clear
@change="() => updateLoopBase()"
/>
</FormItem>
<!-- add by 芋艿由于元素变量暂时用不到所以这里 display none -->
<FormItem label="元素变量" key="elementVariable" style="display: none">
<Input
v-model:value="loopInstanceForm.elementVariable"
allow-clear
@change="() => updateLoopBase()"
/>
</FormItem>
<FormItem label="完成条件" key="completionCondition">
<Input
v-model:value="loopInstanceForm.completionCondition"
allow-clear
@change="
() =>
updateLoopCondition(loopInstanceForm.completionCondition || '')
"
/>
</FormItem>
<!-- add by 芋艿由于异步状态暂时用不到所以这里 display none -->
<FormItem label="异步状态" key="async" style="display: none">
<Checkbox
v-model:checked="loopInstanceForm.asyncBefore"
@change="() => updateLoopAsync('asyncBefore')"
>
异步前
</Checkbox>
<Checkbox
v-model:checked="loopInstanceForm.asyncAfter"
@change="() => updateLoopAsync('asyncAfter')"
>
异步后
</Checkbox>
<Checkbox
v-model:checked="loopInstanceForm.exclusive"
v-if="loopInstanceForm.asyncAfter || loopInstanceForm.asyncBefore"
@change="() => updateLoopAsync('exclusive')"
>
排除
</Checkbox>
</FormItem>
<FormItem
label="重试周期"
prop="timeCycle"
v-if="loopInstanceForm.asyncAfter || loopInstanceForm.asyncBefore"
key="timeCycle"
>
<Input
v-model:value="loopInstanceForm.timeCycle"
allow-clear
@change="
() => updateLoopTimeCycle(loopInstanceForm.timeCycle || '')
"
/>
</FormItem>
</template>
</Form>
</div>
</template>

View File

@ -0,0 +1,74 @@
<script lang="ts" setup>
import { nextTick, onBeforeUnmount, ref, toRaw, watch } from 'vue';
import { Input } from 'ant-design-vue';
defineOptions({ name: 'ElementOtherConfig' });
const props = defineProps({
id: {
type: String,
default: '',
},
});
const { Textarea } = Input;
const documentation = ref('');
const bpmnElement = ref();
const bpmnInstances = () => (window as any).bpmnInstances;
const updateDocumentation = () => {
(bpmnElement.value && bpmnElement.value.id === props.id) ||
(bpmnElement.value = bpmnInstances().elementRegistry.get(props.id));
const documentations = bpmnInstances().bpmnFactory.create(
'bpmn:Documentation',
{
text: documentation.value,
},
);
bpmnInstances().modeling.updateProperties(toRaw(bpmnElement.value), {
documentation: [documentations],
});
};
onBeforeUnmount(() => {
bpmnElement.value = null;
});
watch(
() => props.id,
(id) => {
if (id && id.length > 0) {
nextTick(() => {
const documentations =
bpmnInstances().bpmnElement.businessObject?.documentation;
documentation.value =
documentations && documentations.length > 0
? documentations[0].text
: '';
});
} else {
documentation.value = '';
}
},
{ immediate: true },
);
</script>
<template>
<div class="panel-tab__content">
<div class="element-property input-property">
<div class="element-property__label">元素文档</div>
<div class="element-property__value">
<Textarea
v-model:value="documentation"
:auto-size="{ minRows: 2, maxRows: 4 }"
@change="updateDocumentation"
@blur="updateDocumentation"
/>
</div>
</div>
</div>
</template>

View File

@ -0,0 +1,236 @@
<script lang="ts" setup>
import { inject, nextTick, ref, toRaw, watch } from 'vue';
import { IconifyIcon } from '@vben/icons';
import { cloneDeep } from '@vben/utils';
import {
Button,
Divider,
Form,
FormItem,
Input,
Modal,
Table,
TableColumn,
} from 'ant-design-vue';
defineOptions({ name: 'ElementProperties' });
const props = defineProps({
id: {
type: String,
default: '',
},
type: {
type: String,
default: '',
},
});
const prefix = inject('prefix');
// const width = inject('width')
const elementPropertyList = ref<Array<{ name: string; value: string }>>([]);
const propertyForm = ref<{ name?: string; value?: string }>({});
const editingPropertyIndex = ref(-1);
const propertyFormModelVisible = ref(false);
const bpmnElement = ref<any>();
const otherExtensionList = ref<any[]>([]);
const bpmnElementProperties = ref<any[]>([]);
const bpmnElementPropertyList = ref<any[]>([]);
const attributeFormRef = ref<any>();
const bpmnInstances = () => (window as any)?.bpmnInstances;
const resetAttributesList = () => {
bpmnElement.value = bpmnInstances().bpmnElement;
otherExtensionList.value = []; //
bpmnElementProperties.value =
// bpmnElement.value.businessObject?.extensionElements?.filter((ex) => {
bpmnElement.value.businessObject?.extensionElements?.values?.filter(
(ex: any) => {
if (ex.$type !== `${prefix}:Properties`) {
otherExtensionList.value.push(ex);
}
return ex.$type === `${prefix}:Properties`;
},
) ?? [];
//
bpmnElementPropertyList.value = bpmnElementProperties.value.flatMap(
(current: any) => current.values,
);
//
elementPropertyList.value = cloneDeep(bpmnElementPropertyList.value ?? []);
};
const openAttributesForm = (
attr: null | { name: string; value: string },
index: number,
) => {
editingPropertyIndex.value = index;
// @ts-ignore
propertyForm.value = index === -1 ? {} : cloneDeep(attr);
propertyFormModelVisible.value = true;
nextTick(() => {
if (attributeFormRef.value) attributeFormRef.value.clearValidate();
});
};
const removeAttributes = (
_attr: { name: string; value: string },
index: number,
) => {
Modal.confirm({
title: '提示',
content: '确认移除该属性吗?',
okText: '确 认',
cancelText: '取 消',
onOk() {
elementPropertyList.value.splice(index, 1);
bpmnElementPropertyList.value.splice(index, 1);
//
const propertiesObject = bpmnInstances().moddle.create(
`${prefix}:Properties`,
{
values: bpmnElementPropertyList.value,
},
);
updateElementExtensions(propertiesObject);
resetAttributesList();
},
onCancel() {
// console.info('');
},
});
};
const saveAttribute = () => {
// console.log(propertyForm.value, 'propertyForm.value');
const { name, value } = propertyForm.value;
if (editingPropertyIndex.value === -1) {
//
const newPropertyObject = bpmnInstances().moddle.create(
`${prefix}:Property`,
{
name,
value,
},
);
//
const propertiesObject = bpmnInstances().moddle.create(
`${prefix}:Properties`,
{
values: [...bpmnElementPropertyList.value, newPropertyObject],
},
);
updateElementExtensions(propertiesObject);
} else {
bpmnInstances().modeling.updateModdleProperties(
toRaw(bpmnElement.value),
toRaw(bpmnElementPropertyList.value)[toRaw(editingPropertyIndex.value)],
{
name,
value,
},
);
}
propertyFormModelVisible.value = false;
resetAttributesList();
};
const updateElementExtensions = (properties: any) => {
const extensions = bpmnInstances().moddle.create('bpmn:ExtensionElements', {
values: [...otherExtensionList.value, properties],
});
bpmnInstances().modeling.updateProperties(toRaw(bpmnElement.value), {
extensionElements: extensions,
});
};
watch(
() => props.id,
(val) => {
if (val) {
val && val.length > 0 && resetAttributesList();
}
},
{ immediate: true },
);
</script>
<template>
<div class="panel-tab__content">
<Table :data="elementPropertyList" :scroll="{ y: 240 }" bordered>
<TableColumn title="序号" width="50">
<template #default="{ index }">
{{ index + 1 }}
</template>
</TableColumn>
<TableColumn
title="属性名"
data-index="name"
:min-width="100"
:ellipsis="{ showTitle: true }"
/>
<TableColumn
title="属性值"
data-index="value"
:min-width="100"
:ellipsis="{ showTitle: true }"
/>
<TableColumn title="操作" width="110">
<template #default="{ record, index }">
<Button
type="link"
@click="openAttributesForm(record, index)"
size="small"
>
编辑
</Button>
<Divider type="vertical" />
<Button
type="link"
size="small"
danger
@click="removeAttributes(record, index)"
>
移除
</Button>
</template>
</TableColumn>
</Table>
<div class="element-drawer__button">
<Button type="primary" @click="openAttributesForm(null, -1)">
<template #icon>
<IconifyIcon icon="ep:plus" />
</template>
添加属性
</Button>
</div>
<Modal
v-model:open="propertyFormModelVisible"
title="属性配置"
:width="600"
:destroy-on-close="true"
>
<Form
:model="propertyForm"
ref="attributeFormRef"
:label-col="{ span: 6 }"
>
<FormItem label="属性名:" name="name">
<Input v-model:value="propertyForm.name" allow-clear />
</FormItem>
<FormItem label="属性值:" name="value">
<Input v-model:value="propertyForm.value" allow-clear />
</FormItem>
</Form>
<template #footer>
<Button @click="propertyFormModelVisible = false"> </Button>
<Button type="primary" @click="saveAttribute"> </Button>
</template>
</Modal>
</div>
</template>

View File

@ -0,0 +1,178 @@
<script lang="ts" setup>
import { computed, onMounted, ref } from 'vue';
import { IconifyIcon } from '@vben/icons';
import {
Button,
Form,
FormItem,
Input,
message,
Modal,
Table,
TableColumn,
} from 'ant-design-vue';
defineOptions({ name: 'SignalAndMassage' });
const signalList = ref<any[]>([]);
const messageList = ref<any[]>([]);
const dialogVisible = ref(false);
const modelType = ref('');
const modelObjectForm = ref<any>({});
const rootElements = ref();
const messageIdMap = ref();
const signalIdMap = ref();
const modelConfig = computed(() => {
return modelType.value === 'message'
? { title: '创建消息', idLabel: '消息ID', nameLabel: '消息名称' }
: { title: '创建信号', idLabel: '信号ID', nameLabel: '信号名称' };
});
const bpmnInstances = () => (window as any)?.bpmnInstances;
const initDataList = () => {
// console.log(window, 'window');
rootElements.value = bpmnInstances().modeler.getDefinitions().rootElements;
messageIdMap.value = {};
signalIdMap.value = {};
messageList.value = [];
signalList.value = [];
rootElements.value.forEach((el: any) => {
if (el.$type === 'bpmn:Message') {
messageIdMap.value[el.id] = true;
messageList.value.push({ ...el });
}
if (el.$type === 'bpmn:Signal') {
signalIdMap.value[el.id] = true;
signalList.value.push({ ...el });
}
});
};
const openModel = (type: any) => {
modelType.value = type;
modelObjectForm.value = {};
dialogVisible.value = true;
};
const addNewObject = () => {
if (modelType.value === 'message') {
if (messageIdMap.value[modelObjectForm.value.id]) {
message.error('该消息已存在请修改id后重新保存');
}
const messageRef = bpmnInstances().moddle.create(
'bpmn:Message',
modelObjectForm.value,
);
rootElements.value.push(messageRef);
} else {
if (signalIdMap.value[modelObjectForm.value.id]) {
message.error('该信号已存在请修改id后重新保存');
}
const signalRef = bpmnInstances().moddle.create(
'bpmn:Signal',
modelObjectForm.value,
);
rootElements.value.push(signalRef);
}
dialogVisible.value = false;
initDataList();
};
onMounted(() => {
initDataList();
});
</script>
<template>
<div class="panel-tab__content">
<div class="panel-tab__content--title">
<span>
<IconifyIcon icon="ep:menu" style="margin-right: 8px; color: #555" />
消息列表
</span>
<Button type="primary" title="创建新消息" @click="openModel('message')">
<template #icon>
<IconifyIcon icon="ep:plus" />
</template>
创建新消息
</Button>
</div>
<Table :data-source="messageList" :bordered="true" :pagination="false">
<TableColumn title="序号" width="60px">
<template #default="{ index }">
{{ index + 1 }}
</template>
</TableColumn>
<TableColumn
title="消息ID"
data-index="id"
:width="300"
:ellipsis="{ showTitle: true }"
/>
<TableColumn
title="消息名称"
data-index="name"
:width="300"
:ellipsis="{ showTitle: true }"
/>
</Table>
<div
class="panel-tab__content--title"
style="padding-top: 8px; margin-top: 8px; border-top: 1px solid #eee"
>
<span>
<IconifyIcon icon="ep:menu" style="margin-right: 8px; color: #555">
信号列表
</IconifyIcon>
</span>
<Button type="primary" title="创建新信号" @click="openModel('signal')">
<template #icon>
<IconifyIcon icon="ep:plus" />
</template>
创建新信号
</Button>
</div>
<Table :data-source="signalList" :bordered="true" :pagination="false">
<TableColumn title="序号" width="60px">
<template #default="{ index }">
{{ index + 1 }}
</template>
</TableColumn>
<TableColumn
title="信号ID"
data-index="id"
:width="300"
:ellipsis="{ showTitle: true }"
/>
<TableColumn
title="信号名称"
data-index="name"
:width="300"
:ellipsis="{ showTitle: true }"
/>
</Table>
<Modal
v-model:open="dialogVisible"
:title="modelConfig.title"
:mask-closable="false"
width="400px"
:destroy-on-close="true"
>
<Form
:model="modelObjectForm"
:label-col="{ span: 9 }"
:wrapper-col="{ span: 15 }"
>
<FormItem :label="modelConfig.idLabel">
<Input v-model:value="modelObjectForm.id" allow-clear />
</FormItem>
<FormItem :label="modelConfig.nameLabel">
<Input v-model:value="modelObjectForm.name" allow-clear />
</FormItem>
</Form>
<template #footer>
<Button @click="dialogVisible = false"> </Button>
<Button type="primary" @click="addNewObject"> </Button>
</template>
</Modal>
</div>
</template>

View File

@ -0,0 +1,92 @@
<script lang="ts" setup>
import { ref, watch } from 'vue';
import { Checkbox, Form, FormItem } from 'ant-design-vue';
import { installedComponent } from './data';
defineOptions({ name: 'ElementTaskConfig' });
const props = defineProps({
id: {
type: String,
default: '',
},
type: {
type: String,
default: '',
},
});
const taskConfigForm = ref({
asyncAfter: false,
asyncBefore: false,
exclusive: false,
});
const witchTaskComponent = ref();
const bpmnElement = ref();
const bpmnInstances = () => (window as any).bpmnInstances;
const changeTaskAsync = () => {
if (!taskConfigForm.value.asyncBefore && !taskConfigForm.value.asyncAfter) {
taskConfigForm.value.exclusive = false;
}
bpmnInstances().modeling.updateProperties(bpmnInstances().bpmnElement, {
...taskConfigForm.value,
});
};
watch(
() => props.id,
() => {
bpmnElement.value = bpmnInstances().bpmnElement;
taskConfigForm.value.asyncBefore =
bpmnElement.value?.businessObject?.asyncBefore;
taskConfigForm.value.asyncAfter =
bpmnElement.value?.businessObject?.asyncAfter;
taskConfigForm.value.exclusive =
bpmnElement.value?.businessObject?.exclusive;
},
{ immediate: true },
);
watch(
() => props.type,
() => {
if (props.type) {
// @ts-ignore
witchTaskComponent.value = installedComponent[props.type].component;
}
},
{ immediate: true },
);
</script>
<template>
<div class="panel-tab__content">
<Form :label-col="{ span: 9 }" :wrapper-col="{ span: 15 }">
<!-- add by 芋艿由于异步延续暂时用不到所以这里 display none -->
<FormItem label="异步延续" style="display: none">
<Checkbox
v-model:checked="taskConfigForm.asyncBefore"
@change="changeTaskAsync"
>
异步前
</Checkbox>
<Checkbox
v-model:checked="taskConfigForm.asyncAfter"
@change="changeTaskAsync"
>
异步后
</Checkbox>
<Checkbox
v-model:checked="taskConfigForm.exclusive"
v-if="taskConfigForm.asyncAfter || taskConfigForm.asyncBefore"
@change="changeTaskAsync"
>
排除
</Checkbox>
</FormItem>
<component :is="witchTaskComponent" v-bind="$props" />
</Form>
</div>
</template>

View File

@ -0,0 +1,40 @@
import CallActivity from './task-components/CallActivity.vue';
import ReceiveTask from './task-components/ReceiveTask.vue';
import ScriptTask from './task-components/ScriptTask.vue';
import ServiceTask from './task-components/ServiceTask.vue';
import UserTask from './task-components/UserTask.vue';
export const installedComponent = {
UserTask: {
name: '用户任务',
component: UserTask,
},
ServiceTask: {
name: '服务任务',
component: ServiceTask,
},
ScriptTask: {
name: '脚本任务',
component: ScriptTask,
},
ReceiveTask: {
name: '接收任务',
component: ReceiveTask,
},
CallActivity: {
name: '调用活动',
component: CallActivity,
},
};
export const getTaskCollapseItemName = (
elementType: keyof typeof installedComponent,
) => {
return installedComponent[elementType].name;
};
export const isTaskCollapseItemShow = (
elementType: keyof typeof installedComponent,
) => {
return installedComponent[elementType];
};

View File

@ -0,0 +1,361 @@
<script lang="ts" setup>
import { h, inject, nextTick, ref, toRaw, watch } from 'vue';
import { alert } from '@vben/common-ui';
import { PlusOutlined } from '@vben/icons';
import {
Button,
Divider,
Form,
FormItem,
Input,
Modal,
Switch,
Table,
TableColumn,
} from 'ant-design-vue';
interface FormData {
processInstanceName: string;
calledElement: string;
inheritVariables: boolean;
businessKey: string;
inheritBusinessKey: boolean;
calledElementType: string;
}
defineOptions({ name: 'CallActivity' });
const props = defineProps({
id: { type: String, default: '' },
type: { type: String, default: '' },
});
const prefix = inject('prefix');
const formData = ref<FormData>({
processInstanceName: '',
calledElement: '',
inheritVariables: false,
businessKey: '',
inheritBusinessKey: false,
calledElementType: 'key',
});
const inVariableList = ref<any[]>([]);
const outVariableList = ref<any[]>([]);
const variableType = ref<string>(); //
const editingVariableIndex = ref<number>(-1); //
const variableDialogVisible = ref<boolean>(false);
const varialbeFormRef = ref<any>();
const varialbeFormData = ref<{
source: string;
target: string;
}>({
source: '',
target: '',
});
const bpmnInstances = () => (window as any)?.bpmnInstances;
const bpmnElement = ref<any>();
const otherExtensionList = ref<any[]>([]);
const initCallActivity = () => {
bpmnElement.value = bpmnInstances().bpmnElement;
// console.log(bpmnElement.value.businessObject, 'callActivity');
//
Object.keys(formData.value).forEach((key: string) => {
// @ts-ignore
formData.value[key] =
bpmnElement.value.businessObject[key] ??
formData.value[key as keyof FormData];
});
otherExtensionList.value = []; //
inVariableList.value.length = 0;
outVariableList.value.length = 0;
//
bpmnElement.value.businessObject?.extensionElements?.values?.forEach(
(ex: any) => {
if (ex.$type === `${prefix}:In`) {
inVariableList.value.push(ex);
} else if (ex.$type === `${prefix}:Out`) {
outVariableList.value.push(ex);
} else {
otherExtensionList.value.push(ex);
}
},
);
//
// bpmnInstances().modeling.updateProperties(toRaw(bpmnElement.value), {
// calledElementType: 'key'
// })
};
const updateCallActivityAttr = (attr: keyof FormData) => {
bpmnInstances().modeling.updateProperties(toRaw(bpmnElement.value), {
[attr]: formData.value[attr],
});
};
const openVariableForm = (type: string, data: any, index: number) => {
editingVariableIndex.value = index;
variableType.value = type;
varialbeFormData.value = index === -1 ? {} : { ...data };
variableDialogVisible.value = true;
};
const removeVariable = async (type: string, index: number) => {
try {
await alert('是否确认删除?');
if (type === 'in') {
inVariableList.value.splice(index, 1);
}
if (type === 'out') {
outVariableList.value.splice(index, 1);
}
updateElementExtensions();
} catch {}
};
const saveVariable = () => {
if (editingVariableIndex.value === -1) {
if (variableType.value === 'in') {
inVariableList.value.push(
bpmnInstances().moddle.create(`${prefix}:In`, {
...varialbeFormData.value,
}),
);
}
if (variableType.value === 'out') {
outVariableList.value.push(
bpmnInstances().moddle.create(`${prefix}:Out`, {
...varialbeFormData.value,
}),
);
}
updateElementExtensions();
} else {
if (variableType.value === 'in') {
inVariableList.value[editingVariableIndex.value].source =
varialbeFormData.value.source;
inVariableList.value[editingVariableIndex.value].target =
varialbeFormData.value.target;
}
if (variableType.value === 'out') {
outVariableList.value[editingVariableIndex.value].source =
varialbeFormData.value.source;
outVariableList.value[editingVariableIndex.value].target =
varialbeFormData.value.target;
}
}
variableDialogVisible.value = false;
};
const updateElementExtensions = () => {
const extensions = bpmnInstances().moddle.create('bpmn:ExtensionElements', {
values: [
...inVariableList.value,
...outVariableList.value,
...otherExtensionList.value,
],
});
bpmnInstances().modeling.updateProperties(toRaw(bpmnElement.value), {
extensionElements: extensions,
});
};
watch(
() => props.id,
(val) => {
val &&
val.length > 0 &&
nextTick(() => {
initCallActivity();
});
},
{ immediate: true },
);
</script>
<template>
<div>
<Form :label-col="{ span: 6 }" :wrapper-col="{ span: 18 }">
<FormItem label="实例名称">
<Input
v-model:value="formData.processInstanceName"
allow-clear
placeholder="请输入实例名称"
@change="updateCallActivityAttr('processInstanceName')"
/>
</FormItem>
<!-- TODO 需要可选择已存在的流程 -->
<FormItem label="被调用流程">
<Input
v-model:value="formData.calledElement"
allow-clear
placeholder="请输入被调用流程"
@change="updateCallActivityAttr('calledElement')"
/>
</FormItem>
<FormItem label="继承变量">
<Switch
v-model:checked="formData.inheritVariables"
@change="updateCallActivityAttr('inheritVariables')"
/>
</FormItem>
<FormItem label="继承业务键">
<Switch
v-model:checked="formData.inheritBusinessKey"
@change="updateCallActivityAttr('inheritBusinessKey')"
/>
</FormItem>
<FormItem v-if="!formData.inheritBusinessKey" label="业务键表达式">
<Input
v-model:value="formData.businessKey"
allow-clear
placeholder="请输入业务键表达式"
@change="updateCallActivityAttr('businessKey')"
/>
</FormItem>
<Divider />
<div>
<div class="mb-10px flex">
<span>输入参数</span>
<Button
class="ml-auto"
type="primary"
:icon="h(PlusOutlined)"
title="添加参数"
size="small"
@click="openVariableForm('in', null, -1)"
/>
</div>
<Table
:data-source="inVariableList"
:scroll="{ y: 240 }"
bordered
:pagination="false"
>
<TableColumn
title="源"
data-index="source"
:min-width="100"
:ellipsis="true"
/>
<TableColumn
title="目标"
data-index="target"
:min-width="100"
:ellipsis="true"
/>
<TableColumn title="操作" :width="110">
<template #default="{ record, index }">
<Button
type="link"
@click="openVariableForm('in', record, index)"
size="small"
>
编辑
</Button>
<Divider type="vertical" />
<Button
type="link"
size="small"
danger
@click="removeVariable('in', index)"
>
移除
</Button>
</template>
</TableColumn>
</Table>
</div>
<Divider />
<div>
<div class="mb-10px flex">
<span>输出参数</span>
<Button
class="ml-auto"
type="primary"
:icon="h(PlusOutlined)"
title="添加参数"
size="small"
@click="openVariableForm('out', null, -1)"
/>
</div>
<Table
:data-source="outVariableList"
:scroll="{ y: 240 }"
bordered
:pagination="false"
>
<TableColumn
title="源"
data-index="source"
:min-width="100"
:ellipsis="true"
/>
<TableColumn
title="目标"
data-index="target"
:min-width="100"
:ellipsis="true"
/>
<TableColumn title="操作" :width="110">
<template #default="{ record, index }">
<Button
type="link"
@click="openVariableForm('out', record, index)"
size="small"
>
编辑
</Button>
<Divider type="vertical" />
<Button
type="link"
size="small"
danger
@click="removeVariable('out', index)"
>
移除
</Button>
</template>
</TableColumn>
</Table>
</div>
</Form>
<!-- 添加或修改参数 -->
<Modal
v-model:open="variableDialogVisible"
title="参数配置"
:width="600"
:destroy-on-close="true"
@ok="saveVariable"
@cancel="variableDialogVisible = false"
>
<Form
:model="varialbeFormData"
:label-col="{ span: 6 }"
:wrapper-col="{ span: 18 }"
ref="varialbeFormRef"
>
<FormItem label="源:" name="source">
<Input v-model:value="varialbeFormData.source" allow-clear />
</FormItem>
<FormItem label="目标:" name="target">
<Input v-model:value="varialbeFormData.target" allow-clear />
</FormItem>
</Form>
</Modal>
</div>
</template>
<style lang="scss" scoped></style>

View File

@ -0,0 +1,96 @@
<!-- 表达式选择 -->
<script setup lang="ts">
import type { BpmProcessExpressionApi } from '#/api/bpm/processExpression';
import { reactive, ref } from 'vue';
import { CommonStatusEnum } from '@vben/constants';
import { Button, Modal, Pagination, Table, TableColumn } from 'ant-design-vue';
import { getProcessExpressionPage } from '#/api/bpm/processExpression';
import { ContentWrap } from '#/components/content-wrap';
/** BPM 流程 表单 */
defineOptions({ name: 'ProcessExpressionDialog' });
/** 提交表单 */
const emit = defineEmits(['select']);
const dialogVisible = ref(false); //
const loading = ref(true); //
const list = ref<BpmProcessExpressionApi.ProcessExpression[]>([]); //
const total = ref(0); //
const queryParams = reactive({
pageNo: 1,
pageSize: 10,
type: '',
status: CommonStatusEnum.ENABLE,
});
/** 打开弹窗 */
const open = (type: string) => {
queryParams.pageNo = 1;
queryParams.type = type;
getList();
dialogVisible.value = true;
};
defineExpose({ open }); // open
/** 查询列表 */
const getList = async () => {
loading.value = true;
try {
const data = await getProcessExpressionPage(queryParams);
list.value = data.list;
total.value = data.total;
} finally {
loading.value = false;
}
};
// select
const select = async (row: BpmProcessExpressionApi.ProcessExpression) => {
dialogVisible.value = false;
//
emit('select', row);
};
// const handleCancel = () => {
// dialogVisible.value = false;
// };
</script>
<template>
<Modal
title="请选择表达式"
v-model:open="dialogVisible"
width="1024px"
:footer="null"
>
<ContentWrap>
<Table
:loading="loading"
:data-source="list"
:pagination="false"
:scroll="{ x: 'max-content' }"
>
<TableColumn title="名字" align="center" data-index="name" />
<TableColumn title="表达式" align="center" data-index="expression" />
<TableColumn title="操作" align="center">
<template #default="{ record }">
<Button type="primary" @click="select(record)"> </Button>
</template>
</TableColumn>
</Table>
<!-- 分页 -->
<div class="mt-4 flex justify-end">
<Pagination
:total="total"
v-model:current="queryParams.pageNo"
v-model:page-size="queryParams.pageSize"
show-size-changer
@change="getList"
/>
</div>
</ContentWrap>
</Modal>
</template>

View File

@ -0,0 +1,161 @@
<script lang="ts" setup>
import {
h,
nextTick,
onBeforeUnmount,
onMounted,
ref,
toRaw,
watch,
} from 'vue';
import { PlusOutlined } from '@vben/icons';
import {
Button,
Form,
Input,
message,
Modal,
Select,
SelectOption,
} from 'ant-design-vue';
defineOptions({ name: 'ReceiveTask' });
const props = defineProps({
id: { type: String, default: '' },
type: { type: String, default: '' },
});
const bindMessageId = ref('');
const newMessageForm = ref<Record<string, any>>({});
const messageMap = ref<Record<string, any>>({});
const messageModelVisible = ref(false);
const bpmnElement = ref<any>();
const bpmnMessageRefsMap = ref<Record<string, any>>();
const bpmnRootElements = ref<any>();
const bpmnInstances = () => (window as any).bpmnInstances;
const getBindMessage = () => {
bpmnElement.value = bpmnInstances().bpmnElement;
bindMessageId.value =
bpmnElement.value.businessObject?.messageRef?.id || '-1';
};
const openMessageModel = () => {
messageModelVisible.value = true;
newMessageForm.value = {};
};
const createNewMessage = () => {
if (messageMap.value[newMessageForm.value.id]) {
message.error('该消息已存在请修改id后重新保存');
return;
}
const newMessage = bpmnInstances().moddle.create(
'bpmn:Message',
newMessageForm.value,
);
bpmnRootElements.value.push(newMessage);
messageMap.value[newMessageForm.value.id] = newMessageForm.value.name;
// @ts-ignore
if (bpmnMessageRefsMap.value) {
bpmnMessageRefsMap.value[newMessageForm.value.id] = newMessage;
}
messageModelVisible.value = false;
};
const updateTaskMessage = (messageId: string) => {
if (messageId === '-1') {
bpmnInstances().modeling.updateProperties(toRaw(bpmnElement.value), {
messageRef: null,
});
} else {
bpmnInstances().modeling.updateProperties(toRaw(bpmnElement.value), {
messageRef: bpmnMessageRefsMap.value?.[messageId],
});
}
};
onMounted(() => {
bpmnMessageRefsMap.value = Object.create(null);
bpmnRootElements.value =
bpmnInstances().modeler.getDefinitions().rootElements;
bpmnRootElements.value
.filter((el: any) => el.$type === 'bpmn:Message')
.forEach((m: any) => {
// @ts-ignore
if (bpmnMessageRefsMap.value) {
bpmnMessageRefsMap.value[m.id] = m;
}
messageMap.value[m.id] = m.name;
});
messageMap.value['-1'] = '无';
});
onBeforeUnmount(() => {
bpmnElement.value = null;
});
watch(
() => props.id,
() => {
// bpmnElement.value = bpmnInstances().bpmnElement
nextTick(() => {
getBindMessage();
});
},
{ immediate: true },
);
</script>
<template>
<div style="margin-top: 16px">
<Form.Item label="消息实例">
<div
style="
display: flex;
flex-wrap: nowrap;
align-items: center;
justify-content: space-between;
"
>
<Select
v-model:value="bindMessageId"
@change="(value: any) => updateTaskMessage(value)"
>
<SelectOption
v-for="key in Object.keys(messageMap)"
:value="key"
:key="key"
>
{{ messageMap[key] }}
</SelectOption>
</Select>
<Button
type="primary"
:icon="h(PlusOutlined)"
style="margin-left: 8px"
@click="openMessageModel"
/>
</div>
</Form.Item>
<Modal
v-model:open="messageModelVisible"
:mask-closable="false"
title="创建新消息"
width="400px"
:destroy-on-close="true"
>
<Form :model="newMessageForm" size="small" :label-col="{ span: 6 }">
<Form.Item label="消息ID">
<Input v-model:value="newMessageForm.id" allow-clear />
</Form.Item>
<Form.Item label="消息名称">
<Input v-model:value="newMessageForm.name" allow-clear />
</Form.Item>
</Form>
<template #footer>
<Button size="small" type="primary" @click="createNewMessage">
</Button>
</template>
</Modal>
</div>
</template>

View File

@ -0,0 +1,129 @@
<script lang="ts" setup>
import {
defineOptions,
defineProps,
nextTick,
onBeforeUnmount,
ref,
toRaw,
watch,
} from 'vue';
import {
FormItem,
Input,
Select,
SelectOption,
Textarea,
} from 'ant-design-vue';
defineOptions({ name: 'ScriptTask' });
const props = defineProps({
id: {
type: String,
default: '',
},
type: {
type: String,
default: '',
},
});
const defaultTaskForm = ref({
scriptFormat: '',
script: '',
resource: '',
resultVariable: '',
});
const scriptTaskForm = ref<any>({});
const bpmnElement = ref();
const bpmnInstances = () => (window as any)?.bpmnInstances;
const resetTaskForm = () => {
for (const key in defaultTaskForm.value) {
// @ts-ignore
scriptTaskForm.value[key] =
bpmnElement.value?.businessObject[
key as keyof typeof defaultTaskForm.value
] || defaultTaskForm.value[key as keyof typeof defaultTaskForm.value];
}
scriptTaskForm.value.scriptType = scriptTaskForm.value.script
? 'inline'
: 'external';
};
const updateElementTask = () => {
const taskAttr = Object.create(null);
taskAttr.scriptFormat = scriptTaskForm.value.scriptFormat || null;
taskAttr.resultVariable = scriptTaskForm.value.resultVariable || null;
if (scriptTaskForm.value.scriptType === 'inline') {
taskAttr.script = scriptTaskForm.value.script || null;
taskAttr.resource = null;
} else {
taskAttr.resource = scriptTaskForm.value.resource || null;
taskAttr.script = null;
}
bpmnInstances().modeling.updateProperties(toRaw(bpmnElement.value), taskAttr);
};
onBeforeUnmount(() => {
bpmnElement.value = null;
});
watch(
() => props.id,
() => {
bpmnElement.value = bpmnInstances().bpmnElement;
nextTick(() => {
resetTaskForm();
});
},
{ immediate: true },
);
</script>
<template>
<div class="mt-4">
<FormItem label="脚本格式">
<Input
v-model:value="scriptTaskForm.scriptFormat"
allow-clear
@input="updateElementTask()"
@change="updateElementTask()"
/>
</FormItem>
<FormItem label="脚本类型">
<Select v-model:value="scriptTaskForm.scriptType">
<SelectOption value="inline">内联脚本</SelectOption>
<SelectOption value="external">外部资源</SelectOption>
</Select>
</FormItem>
<FormItem label="脚本" v-show="scriptTaskForm.scriptType === 'inline'">
<Textarea
v-model:value="scriptTaskForm.script"
:auto-size="{ minRows: 2, maxRows: 4 }"
allow-clear
@input="updateElementTask()"
@change="updateElementTask()"
/>
</FormItem>
<FormItem
label="资源地址"
v-show="scriptTaskForm.scriptType === 'external'"
>
<Input
v-model:value="scriptTaskForm.resource"
allow-clear
@input="updateElementTask()"
@change="updateElementTask()"
/>
</FormItem>
<FormItem label="结果变量">
<Input
v-model:value="scriptTaskForm.resultVariable"
allow-clear
@input="updateElementTask()"
@change="updateElementTask()"
/>
</FormItem>
</div>
</template>

View File

@ -0,0 +1,111 @@
<script lang="ts" setup>
import { nextTick, onBeforeUnmount, ref, toRaw, watch } from 'vue';
import { FormItem, Input, Select } from 'ant-design-vue';
defineOptions({ name: 'ServiceTask' });
const props = defineProps({
id: { type: String, default: '' },
type: { type: String, default: '' },
});
const defaultTaskForm = ref({
executeType: '',
class: '',
expression: '',
delegateExpression: '',
});
const serviceTaskForm = ref<any>({});
const bpmnElement = ref();
const bpmnInstances = () => (window as any)?.bpmnInstances;
const resetTaskForm = () => {
for (const key in defaultTaskForm.value) {
const value =
// @ts-ignore
bpmnElement.value?.businessObject[key] || defaultTaskForm.value[key];
serviceTaskForm.value[key] = value;
if (value) {
serviceTaskForm.value.executeType = key;
}
}
};
const updateElementTask = () => {
const taskAttr = Object.create(null);
const type = serviceTaskForm.value.executeType;
for (const key in serviceTaskForm.value) {
if (key !== 'executeType' && key !== type) taskAttr[key] = null;
}
taskAttr[type] = serviceTaskForm.value[type] || '';
bpmnInstances().modeling.updateProperties(toRaw(bpmnElement.value), taskAttr);
};
onBeforeUnmount(() => {
bpmnElement.value = null;
});
watch(
() => props.id,
() => {
bpmnElement.value = bpmnInstances().bpmnElement;
nextTick(() => {
resetTaskForm();
});
},
{ immediate: true },
);
</script>
<template>
<div>
<FormItem label="执行类型" key="executeType">
<Select
v-model:value="serviceTaskForm.executeType"
:options="[
{ label: 'Java类', value: 'class' },
{ label: '表达式', value: 'expression' },
{ label: '代理表达式', value: 'delegateExpression' },
]"
/>
</FormItem>
<FormItem
v-if="serviceTaskForm.executeType === 'class'"
label="Java类"
name="class"
key="execute-class"
>
<Input
v-model:value="serviceTaskForm.class"
allow-clear
@change="updateElementTask"
/>
</FormItem>
<FormItem
v-if="serviceTaskForm.executeType === 'expression'"
label="表达式"
name="expression"
key="execute-expression"
>
<Input
v-model:value="serviceTaskForm.expression"
allow-clear
@change="updateElementTask"
/>
</FormItem>
<FormItem
v-if="serviceTaskForm.executeType === 'delegateExpression'"
label="代理表达式"
name="delegateExpression"
key="execute-delegate"
>
<Input
v-model:value="serviceTaskForm.delegateExpression"
allow-clear
@change="updateElementTask"
/>
</FormItem>
</div>
</template>

View File

@ -0,0 +1,571 @@
<script lang="ts" setup>
import type { BpmProcessExpressionApi } from '#/api/bpm/processExpression';
import type { BpmUserGroupApi } from '#/api/bpm/userGroup';
import type { SystemPostApi } from '#/api/system/post';
import type { SystemRoleApi } from '#/api/system/role';
import type { SystemUserApi } from '#/api/system/user';
import {
computed,
h,
inject,
nextTick,
onBeforeUnmount,
onMounted,
ref,
toRaw,
watch,
} from 'vue';
import { SelectOutlined } from '@vben/icons';
import { handleTree } from '@vben/utils';
import {
Button,
Form,
FormItem,
Select,
SelectOption,
Textarea,
TreeSelect,
} from 'ant-design-vue';
import { getUserGroupSimpleList } from '#/api/bpm/userGroup';
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 {
CANDIDATE_STRATEGY,
CandidateStrategy,
FieldPermissionType,
MULTI_LEVEL_DEPT,
} from '#/components/simple-process-design/consts';
import { useFormFieldsPermission } from '#/components/simple-process-design/helpers';
import ProcessExpressionDialog from './ProcessExpressionDialog.vue';
defineOptions({ name: 'UserTask' });
const props = defineProps({
id: {
type: String,
default: '',
},
type: {
type: String,
default: '',
},
});
const prefix = inject('prefix');
const userTaskForm = ref({
candidateStrategy: undefined, //
candidateParam: [], //
skipExpression: '', //
});
const bpmnElement = ref<any>();
const bpmnInstances = () => (window as Record<string, any>)?.bpmnInstances;
const roleOptions = ref<SystemRoleApi.Role[]>([]); //
const deptTreeOptions = ref<any>(); //
const postOptions = ref<SystemPostApi.Post[]>([]); //
const userOptions = ref<SystemUserApi.User[]>([]); //
const userGroupOptions = ref<BpmUserGroupApi.UserGroup[]>([]); //
const treeRef = ref<any>();
const { formFieldOptions } = useFormFieldsPermission(FieldPermissionType.READ);
// TreeSelect
const defaultProps = {
children: 'children',
label: 'name',
value: 'id',
};
// ,
const userFieldOnFormOptions = computed(() => {
return formFieldOptions.filter((item) => item.type === 'UserSelect');
});
// ,
const deptFieldOnFormOptions = computed(() => {
return formFieldOptions.filter((item) => item.type === 'DeptSelect');
});
const deptLevel = ref(1);
const deptLevelLabel = computed(() => {
let label = '部门负责人来源';
if (
userTaskForm.value.candidateStrategy ===
CandidateStrategy.MULTI_LEVEL_DEPT_LEADER
) {
label = `${label}(指定部门向上)`;
} else if (
userTaskForm.value.candidateStrategy === CandidateStrategy.FORM_DEPT_LEADER
) {
label = `${label}(表单内部门向上)`;
} else {
label = `${label}(发起人部门向上)`;
}
return label;
});
const otherExtensions = ref<any>();
const resetTaskForm = () => {
const businessObject = bpmnElement.value.businessObject;
if (!businessObject) {
return;
}
const extensionElements =
businessObject?.extensionElements ??
bpmnInstances().moddle.create('bpmn:ExtensionElements', { values: [] });
userTaskForm.value.candidateStrategy = extensionElements.values?.filter(
(ex: any) => ex.$type === `${prefix}:CandidateStrategy`,
)?.[0]?.value;
const candidateParamStr = extensionElements.values?.filter(
(ex: any) => ex.$type === `${prefix}:CandidateParam`,
)?.[0]?.value;
if (candidateParamStr && candidateParamStr.length > 0) {
// eslint-disable-next-line unicorn/prefer-switch
if (userTaskForm.value.candidateStrategy === CandidateStrategy.EXPRESSION) {
// input
// @ts-ignore
userTaskForm.value.candidateParam = [candidateParamStr];
} else if (
userTaskForm.value.candidateStrategy ===
CandidateStrategy.MULTI_LEVEL_DEPT_LEADER
) {
// '|'
userTaskForm.value.candidateParam = candidateParamStr
.split('|')[0]
.split(',')
.map((item: any) => {
//
const num = Number(item);
return num > Number.MAX_SAFE_INTEGER || num < -Number.MAX_SAFE_INTEGER
? item
: num;
});
deptLevel.value = +candidateParamStr.split('|')[1];
} else if (
userTaskForm.value.candidateStrategy ===
CandidateStrategy.START_USER_DEPT_LEADER ||
userTaskForm.value.candidateStrategy ===
CandidateStrategy.START_USER_MULTI_LEVEL_DEPT_LEADER
) {
// @ts-ignore
userTaskForm.value.candidateParam = +candidateParamStr;
deptLevel.value = +candidateParamStr;
} else if (
userTaskForm.value.candidateStrategy ===
CandidateStrategy.FORM_DEPT_LEADER
) {
userTaskForm.value.candidateParam = candidateParamStr.split('|')[0];
deptLevel.value = +candidateParamStr.split('|')[1];
} else {
userTaskForm.value.candidateParam = candidateParamStr
.split(',')
.map((item: any) => {
//
const num = Number(item);
return num > Number.MAX_SAFE_INTEGER || num < -Number.MAX_SAFE_INTEGER
? item
: num;
});
}
} else {
userTaskForm.value.candidateParam = [];
}
otherExtensions.value =
extensionElements.values?.filter(
(ex: any) =>
ex.$type !== `${prefix}:CandidateStrategy` &&
ex.$type !== `${prefix}:CandidateParam`,
) ?? [];
//
userTaskForm.value.skipExpression =
businessObject.skipExpression === undefined
? ''
: businessObject.skipExpression;
// extensionElements
// if (businessObject.candidateStrategy != undefined) {
// userTaskForm.value.candidateStrategy = parseInt(
// businessObject.candidateStrategy,
// ) as any;
// } else {
// userTaskForm.value.candidateStrategy = undefined;
// }
// if (
// businessObject.candidateParam &&
// businessObject.candidateParam.length > 0
// ) {
// if (userTaskForm.value.candidateStrategy === 60) {
// // input
// userTaskForm.value.candidateParam = [businessObject.candidateParam];
// } else {
// userTaskForm.value.candidateParam = businessObject.candidateParam
// .split(',')
// .map((item) => item);
// }
// } else {
// userTaskForm.value.candidateParam = [];
// }
};
/** 更新 candidateStrategy 字段时,需要清空 candidateParam并触发 bpmn 图更新 */
const changeCandidateStrategy = () => {
userTaskForm.value.candidateParam = [];
deptLevel.value = 1;
// by https://t.zsxq.com/xNmas
// if (userTaskForm.value.candidateStrategy === CandidateStrategy.FORM_USER) {
// //
// if (!userFieldOnFormOptions.value || userFieldOnFormOptions.value.length <= 1) {
// userTaskForm.value.candidateStrategy = CandidateStrategy.START_USER
// }
// }
updateElementTask();
};
/** 选中某个 options 时候,更新 bpmn 图 */
const updateElementTask = () => {
let candidateParam = Array.isArray(userTaskForm.value.candidateParam)
? userTaskForm.value.candidateParam.join(',')
: userTaskForm.value.candidateParam;
//
if (
userTaskForm.value.candidateStrategy ===
CandidateStrategy.MULTI_LEVEL_DEPT_LEADER ||
userTaskForm.value.candidateStrategy === CandidateStrategy.FORM_DEPT_LEADER
) {
candidateParam += `|${deptLevel.value}`;
}
//
if (
userTaskForm.value.candidateStrategy ===
CandidateStrategy.START_USER_DEPT_LEADER ||
userTaskForm.value.candidateStrategy ===
CandidateStrategy.START_USER_MULTI_LEVEL_DEPT_LEADER
) {
candidateParam = `${deptLevel.value}`;
}
const extensions = bpmnInstances().moddle.create('bpmn:ExtensionElements', {
values: [
...otherExtensions.value,
bpmnInstances().moddle.create(`${prefix}:CandidateStrategy`, {
value: userTaskForm.value.candidateStrategy,
}),
bpmnInstances().moddle.create(`${prefix}:CandidateParam`, {
value: candidateParam,
}),
],
});
bpmnInstances().modeling.updateProperties(toRaw(bpmnElement.value), {
extensionElements: extensions,
});
// extensionElements
// return;
// bpmnInstances().modeling.updateProperties(toRaw(bpmnElement.value), {
// candidateStrategy: userTaskForm.value.candidateStrategy,
// candidateParam: userTaskForm.value.candidateParam.join(','),
// });
};
const updateSkipExpression = () => {
if (
userTaskForm.value.skipExpression &&
userTaskForm.value.skipExpression !== ''
) {
bpmnInstances().modeling.updateProperties(toRaw(bpmnElement.value), {
skipExpression: userTaskForm.value.skipExpression,
});
} else {
bpmnInstances().modeling.updateProperties(toRaw(bpmnElement.value), {
skipExpression: null,
});
}
};
//
const processExpressionDialogRef = ref<any>();
const openProcessExpressionDialog = async () => {
processExpressionDialogRef.value.open();
};
const selectProcessExpression = (
expression: BpmProcessExpressionApi.ProcessExpression,
) => {
// @ts-ignore
userTaskForm.value.candidateParam = [expression.expression];
updateElementTask();
};
const handleFormUserChange = (e: any) => {
if (e === 'PROCESS_START_USER_ID') {
userTaskForm.value.candidateParam = [];
// @ts-ignore
userTaskForm.value.candidateStrategy = CandidateStrategy.START_USER;
}
updateElementTask();
};
watch(
() => props.id,
() => {
bpmnElement.value = bpmnInstances().bpmnElement;
nextTick(() => {
resetTaskForm();
});
},
{ immediate: true },
);
onMounted(async () => {
//
roleOptions.value = await getSimpleRoleList();
//
const deptOptions = await getSimpleDeptList();
deptTreeOptions.value = handleTree(deptOptions, 'id');
//
postOptions.value = await getSimplePostList();
//
userOptions.value = await getSimpleUserList();
//
userGroupOptions.value = await getUserGroupSimpleList();
});
onBeforeUnmount(() => {
bpmnElement.value = null;
});
</script>
<template>
<Form :label-col="{ span: 6 }" :wrapper-col="{ span: 18 }">
<FormItem label="规则类型" name="candidateStrategy">
<Select
v-model:value="userTaskForm.candidateStrategy"
allow-clear
style="width: 100%"
@change="changeCandidateStrategy"
>
<SelectOption
v-for="(dict, index) in CANDIDATE_STRATEGY"
:key="index"
:value="dict.value"
>
{{ dict.label }}
</SelectOption>
</Select>
</FormItem>
<FormItem
v-if="userTaskForm.candidateStrategy === CandidateStrategy.ROLE"
label="指定角色"
name="candidateParam"
>
<Select
v-model:value="userTaskForm.candidateParam"
allow-clear
mode="multiple"
style="width: 100%"
@change="updateElementTask"
>
<SelectOption
v-for="item in roleOptions"
:key="item.id"
:value="item.id"
>
{{ item.name }}
</SelectOption>
</Select>
</FormItem>
<FormItem
v-if="
userTaskForm.candidateStrategy === CandidateStrategy.DEPT_MEMBER ||
userTaskForm.candidateStrategy === CandidateStrategy.DEPT_LEADER ||
userTaskForm.candidateStrategy ===
CandidateStrategy.MULTI_LEVEL_DEPT_LEADER
"
label="指定部门"
name="candidateParam"
>
<TreeSelect
ref="treeRef"
v-model:value="userTaskForm.candidateParam"
:tree-data="deptTreeOptions"
:field-names="defaultProps"
placeholder="加载中,请稍后"
multiple
tree-checkable
@change="updateElementTask"
/>
</FormItem>
<FormItem
v-if="userTaskForm.candidateStrategy === CandidateStrategy.POST"
label="指定岗位"
name="candidateParam"
>
<Select
v-model:value="userTaskForm.candidateParam"
allow-clear
mode="multiple"
style="width: 100%"
@change="updateElementTask"
>
<SelectOption
v-for="item in postOptions"
:key="item.id"
:value="item.id"
>
{{ item.name }}
</SelectOption>
</Select>
</FormItem>
<FormItem
v-if="userTaskForm.candidateStrategy === CandidateStrategy.USER"
label="指定用户"
name="candidateParam"
>
<Select
v-model:value="userTaskForm.candidateParam"
allow-clear
mode="multiple"
style="width: 100%"
@change="updateElementTask"
>
<SelectOption
v-for="item in userOptions"
:key="item.id"
:value="item.id"
>
{{ item.nickname }}
</SelectOption>
</Select>
</FormItem>
<FormItem
v-if="userTaskForm.candidateStrategy === CandidateStrategy.USER_GROUP"
label="指定用户组"
name="candidateParam"
>
<Select
v-model:value="userTaskForm.candidateParam"
allow-clear
mode="multiple"
style="width: 100%"
@change="updateElementTask"
>
<SelectOption
v-for="item in userGroupOptions"
:key="item.id"
:value="item.id"
>
{{ item.name }}
</SelectOption>
</Select>
</FormItem>
<FormItem
v-if="userTaskForm.candidateStrategy === CandidateStrategy.FORM_USER"
label="表单内用户字段"
name="formUser"
>
<Select
v-model:value="userTaskForm.candidateParam"
allow-clear
style="width: 100%"
@change="handleFormUserChange"
>
<SelectOption
v-for="(item, idx) in userFieldOnFormOptions"
:key="idx"
:value="item.field"
:disabled="!item.required"
>
{{ item.title }}
</SelectOption>
</Select>
</FormItem>
<FormItem
v-if="
userTaskForm.candidateStrategy === CandidateStrategy.FORM_DEPT_LEADER
"
label="表单内部门字段"
name="formDept"
>
<Select
v-model:value="userTaskForm.candidateParam"
allow-clear
style="width: 100%"
@change="updateElementTask"
>
<SelectOption
v-for="(item, idx) in deptFieldOnFormOptions"
:key="idx"
:value="item.field"
:disabled="!item.required"
>
{{ item.title }}
</SelectOption>
</Select>
</FormItem>
<FormItem
v-if="
userTaskForm.candidateStrategy ===
CandidateStrategy.MULTI_LEVEL_DEPT_LEADER ||
userTaskForm.candidateStrategy ===
CandidateStrategy.START_USER_DEPT_LEADER ||
userTaskForm.candidateStrategy ===
CandidateStrategy.START_USER_MULTI_LEVEL_DEPT_LEADER ||
userTaskForm.candidateStrategy === CandidateStrategy.FORM_DEPT_LEADER
"
:label="deptLevelLabel!"
name="deptLevel"
>
<Select v-model:value="deptLevel" allow-clear @change="updateElementTask">
<SelectOption
v-for="(item, index) in MULTI_LEVEL_DEPT"
:key="index"
:value="item.value"
>
{{ item.label }}
</SelectOption>
</Select>
</FormItem>
<FormItem
v-if="userTaskForm.candidateStrategy === CandidateStrategy.EXPRESSION"
label="流程表达式"
name="candidateParam"
>
<Textarea
v-model:value="userTaskForm.candidateParam[0]"
allow-clear
style="width: 100%"
@change="updateElementTask"
/>
<Button
class="!w-1/1 mt-5px"
type="primary"
:icon="h(SelectOutlined)"
@click="openProcessExpressionDialog"
>
选择表达式
</Button>
<!-- 选择弹窗 -->
<ProcessExpressionDialog
ref="processExpressionDialogRef"
@select="selectProcessExpression"
/>
</FormItem>
<FormItem label="跳过表达式" name="skipExpression">
<Textarea
v-model:value="userTaskForm.skipExpression"
allow-clear
style="width: 100%"
@change="updateSkipExpression"
/>
</FormItem>
</Form>
</template>

View File

@ -0,0 +1,380 @@
<script setup>
import { ref, watch } from 'vue';
import {
Button,
Checkbox,
DatePicker,
Input,
InputNumber,
Radio,
Tabs,
} from 'ant-design-vue';
const props = defineProps({
value: {
type: String,
default: '',
},
});
const emit = defineEmits(['change']);
const tab = ref('cron');
const cronStr = ref(props.value || '* * * * * ?');
const fields = ref({
second: '*',
minute: '*',
hour: '*',
day: '*',
month: '*',
week: '?',
year: '',
});
const cronFieldList = [
{ key: 'second', label: '秒', min: 0, max: 59 },
{ key: 'minute', label: '分', min: 0, max: 59 },
{ key: 'hour', label: '时', min: 0, max: 23 },
{ key: 'day', label: '天', min: 1, max: 31 },
{ key: 'month', label: '月', min: 1, max: 12 },
{ key: 'week', label: '周', min: 1, max: 7 },
{ key: 'year', label: '年', min: 1970, max: 2099 },
];
const activeField = ref('second');
const cronMode = ref({
second: 'appoint',
minute: 'every',
hour: 'every',
day: 'every',
month: 'every',
week: 'every',
year: 'every',
});
const cronAppoint = ref({
second: ['00', '01'],
minute: [],
hour: [],
day: [],
month: [],
week: [],
year: [],
});
const cronRange = ref({
second: [0, 1],
minute: [0, 1],
hour: [0, 1],
day: [1, 2],
month: [1, 2],
week: [1, 2],
year: [1970, 1971],
});
const cronStep = ref({
second: [1, 1],
minute: [1, 1],
hour: [1, 1],
day: [1, 1],
month: [1, 1],
week: [1, 1],
year: [1970, 1],
});
function pad(n) {
return n < 10 ? `0${n}` : `${n}`;
}
watch(
[fields, cronMode, cronAppoint, cronRange, cronStep],
() => {
// cron
const arr = cronFieldList.map((f) => {
if (cronMode.value[f.key] === 'every') return '*';
if (cronMode.value[f.key] === 'appoint')
return cronAppoint.value[f.key].join(',') || '*';
if (cronMode.value[f.key] === 'range')
return `${cronRange.value[f.key][0]}-${cronRange.value[f.key][1]}`;
if (cronMode.value[f.key] === 'step')
return `${cronStep.value[f.key][0]}/${cronStep.value[f.key][1]}`;
return fields.value[f.key] || '*';
});
// weekyear
arr[5] = arr[5] || '?';
cronStr.value = arr.join(' ');
if (tab.value === 'cron') emit('change', cronStr.value);
},
{ deep: true },
);
//
const isoStr = ref('');
const repeat = ref(1);
const isoDate = ref('');
const isoDuration = ref('');
function setDuration(type, val) {
// ISO 8601
let d = isoDuration.value;
if (d.includes(type)) {
d = d.replace(new RegExp(`\\d+${type}`), val + type);
} else {
d += val + type;
}
isoDuration.value = d;
updateIsoStr();
}
function updateIsoStr() {
let str = `R${repeat.value}`;
if (isoDate.value)
str += `/${
typeof isoDate.value === 'string'
? isoDate.value
: new Date(isoDate.value).toISOString()
}`;
if (isoDuration.value) str += `/${isoDuration.value}`;
isoStr.value = str;
if (tab.value === 'iso') emit('change', isoStr.value);
}
watch([repeat, isoDate, isoDuration], updateIsoStr);
watch(
() => props.value,
(val) => {
if (!val) return;
if (tab.value === 'cron') cronStr.value = val;
if (tab.value === 'iso') isoStr.value = val;
},
{ immediate: true },
);
</script>
<template>
<Tabs v-model:active-key="tab">
<Tabs.TabPane key="cron" tab="CRON表达式">
<div style="margin-bottom: 10px">
<Input
v-model:value="cronStr"
readonly
style="width: 400px; font-weight: bold"
key="cronStr"
/>
</div>
<div style="display: flex; gap: 8px; margin-bottom: 8px">
<Input
v-model:value="fields.second"
placeholder="秒"
style="width: 80px"
key="second"
/>
<Input
v-model:value="fields.minute"
placeholder="分"
style="width: 80px"
key="minute"
/>
<Input
v-model:value="fields.hour"
placeholder="时"
style="width: 80px"
key="hour"
/>
<Input
v-model:value="fields.day"
placeholder="天"
style="width: 80px"
key="day"
/>
<Input
v-model:value="fields.month"
placeholder="月"
style="width: 80px"
key="month"
/>
<Input
v-model:value="fields.week"
placeholder="周"
style="width: 80px"
key="week"
/>
<Input
v-model:value="fields.year"
placeholder="年"
style="width: 80px"
key="year"
/>
</div>
<Tabs
v-model:active-key="activeField"
type="card"
style="margin-bottom: 8px"
>
<Tabs.TabPane v-for="f in cronFieldList" :key="f.key" :tab="f.label">
<div style="margin-bottom: 8px">
<Radio.Group
v-model:value="cronMode[f.key]"
:key="`radio-${f.key}`"
>
<Radio value="every" :key="`every-${f.key}`">
{{ f.label }}
</Radio>
<Radio value="range" :key="`range-${f.key}`">
<InputNumber
v-model:value="cronRange[f.key][0]"
:min="f.min"
:max="f.max"
size="small"
style="width: 60px"
:key="`range0-${f.key}`"
/>
<InputNumber
v-model:value="cronRange[f.key][1]"
:min="f.min"
:max="f.max"
size="small"
style="width: 60px"
:key="`range1-${f.key}`"
/>
之间每{{ f.label }}
</Radio>
<Radio value="step" :key="`step-${f.key}`">
从第
<InputNumber
v-model:value="cronStep[f.key][0]"
:min="f.min"
:max="f.max"
size="small"
style="width: 60px"
:key="`step0-${f.key}`"
/>
开始每
<InputNumber
v-model:value="cronStep[f.key][1]"
:min="1"
:max="f.max"
size="small"
style="width: 60px"
:key="`step1-${f.key}`"
/>
{{ f.label }}
</Radio>
<Radio value="appoint" :key="`appoint-${f.key}`"> 指定 </Radio>
</Radio.Group>
</div>
<div v-if="cronMode[f.key] === 'appoint'">
<Checkbox.Group
v-model:value="cronAppoint[f.key]"
:key="`group-${f.key}`"
>
<Checkbox
v-for="n in f.max + 1"
:key="`cb-${f.key}-${n - 1}`"
:value="pad(n - 1)"
>
{{ pad(n - 1) }}
</Checkbox>
</Checkbox.Group>
</div>
</Tabs.TabPane>
</Tabs>
</Tabs.TabPane>
<Tabs.TabPane key="iso" title="标准格式" tab="iso-tab">
<div style="margin-bottom: 10px">
<Input
v-model:value="isoStr"
placeholder="如R1/2025-05-21T21:59:54/P3DT30M30S"
style="width: 400px; font-weight: bold"
key="isoStr"
/>
</div>
<div style="margin-bottom: 10px">
循环次数<InputNumber
v-model:value="repeat"
:min="1"
style="width: 100px"
key="repeat"
/>
</div>
<div style="margin-bottom: 10px">
日期时间<DatePicker
v-model:value="isoDate"
show-time
placeholder="选择日期时间"
style="width: 200px"
key="isoDate"
/>
</div>
<div style="margin-bottom: 10px">
当前时长<Input
v-model:value="isoDuration"
placeholder="如P3DT30M30S"
style="width: 200px"
key="isoDuration"
/>
</div>
<div>
<div>
<Button
v-for="s in [5, 10, 30, 50]"
@click="setDuration('S', s)"
:key="`sec-${s}`"
>
{{ s }}
</Button>
自定义
</div>
<div>
<Button
v-for="m in [5, 10, 30, 50]"
@click="setDuration('M', m)"
:key="`min-${m}`"
>
{{ m }}
</Button>
自定义
</div>
<div>
小时
<Button
v-for="h in [4, 8, 12, 24]"
@click="setDuration('H', h)"
:key="`hour-${h}`"
>
{{ h }}
</Button>
自定义
</div>
<div>
<Button
v-for="d in [1, 2, 3, 4]"
@click="setDuration('D', d)"
:key="`day-${d}`"
>
{{ d }}
</Button>
自定义
</div>
<div>
<Button
v-for="mo in [1, 2, 3, 4]"
@click="setDuration('M', mo)"
:key="`mon-${mo}`"
>
{{ mo }}
</Button>
自定义
</div>
<div>
<Button
v-for="y in [1, 2, 3, 4]"
@click="setDuration('Y', y)"
:key="`year-${y}`"
>
{{ y }}
</Button>
自定义
</div>
</div>
</Tabs.TabPane>
</Tabs>
</template>

View File

@ -0,0 +1,99 @@
<script setup>
import { ref, watch } from 'vue';
import { Button, Input } from 'ant-design-vue';
const props = defineProps({
value: {
type: String,
default: '',
},
});
const emit = defineEmits(['change']);
const units = [
{ key: 'Y', label: '年', presets: [1, 2, 3, 4] },
{ key: 'M', label: '月', presets: [1, 2, 3, 4] },
{ key: 'D', label: '天', presets: [1, 2, 3, 4] },
{ key: 'H', label: '时', presets: [4, 8, 12, 24] },
{ key: 'm', label: '分', presets: [5, 10, 30, 50] },
{ key: 'S', label: '秒', presets: [5, 10, 30, 50] },
];
const custom = ref({ Y: '', M: '', D: '', H: '', m: '', S: '' });
const isoString = ref('');
function setUnit(key, val) {
if (!val || Number.isNaN(val)) {
custom.value[key] = '';
return;
}
custom.value[key] = val;
updateIsoString();
}
function updateIsoString() {
let str = 'P';
if (custom.value.Y) str += `${custom.value.Y}Y`;
if (custom.value.M) str += `${custom.value.M}M`;
if (custom.value.D) str += `${custom.value.D}D`;
if (custom.value.H || custom.value.m || custom.value.S) str += 'T';
if (custom.value.H) str += `${custom.value.H}H`;
if (custom.value.m) str += `${custom.value.m}M`;
if (custom.value.S) str += `${custom.value.S}S`;
isoString.value = str === 'P' ? '' : str;
emit('change', isoString.value);
}
watch(
() => props.value,
(val) => {
if (!val) return;
// ISO 8601custom
const match = val.match(
/^P(?:(\d+)Y)?(?:(\d+)M)?(?:(\d+)D)?(?:T(?:(\d+)H)?(?:(\d+)M)?(?:(\d+)S)?)?$/,
);
if (match) {
custom.value.Y = match[1] || '';
custom.value.M = match[2] || '';
custom.value.D = match[3] || '';
custom.value.H = match[4] || '';
custom.value.m = match[5] || '';
custom.value.S = match[6] || '';
updateIsoString();
}
},
{ immediate: true },
);
</script>
<template>
<div>
<div style="margin-bottom: 10px">
当前选择<Input
v-model:value="isoString"
readonly
style="width: 300px"
/>
</div>
<div v-for="unit in units" :key="unit.key" style="margin-bottom: 8px">
<span>{{ unit.label }}</span>
<Button.Group>
<Button
v-for="val in unit.presets"
:key="val"
size="small"
@click="setUnit(unit.key, val)"
>
{{ val }}
</Button>
<Input
v-model:value="custom[unit.key]"
size="small"
style="width: 60px; margin-left: 8px"
placeholder="自定义"
@change="setUnit(unit.key, custom[unit.key])"
/>
</Button.Group>
</div>
</div>
</template>

View File

@ -0,0 +1,357 @@
<script lang="ts" setup>
import type { Ref } from 'vue';
import { computed, nextTick, onMounted, ref, toRaw, watch } from 'vue';
import {
CheckCircleFilled,
ExclamationCircleFilled,
IconifyIcon,
QuestionCircleFilled,
} from '@vben/icons';
import { Button, DatePicker, Input, Modal, Tooltip } from 'ant-design-vue';
import CycleConfig from './CycleConfig.vue';
import DurationConfig from './DurationConfig.vue';
const props = defineProps({
businessObject: {
type: Object,
default: () => ({}),
},
});
const bpmnInstances = () => (window as any).bpmnInstances;
const type: Ref<string> = ref('time');
const condition: Ref<string> = ref('');
const valid: Ref<boolean> = ref(true);
const showDatePicker: Ref<boolean> = ref(false);
const showDurationDialog: Ref<boolean> = ref(false);
const showCycleDialog: Ref<boolean> = ref(false);
const showHelp: Ref<boolean> = ref(false);
const dateValue: Ref<Date | null> = ref(null);
// const bpmnElement = ref(null);
const placeholder = computed<string>(() => {
if (type.value === 'time') return '请输入时间';
if (type.value === 'duration') return '请输入持续时长';
if (type.value === 'cycle') return '请输入循环表达式';
return '';
});
const helpText = computed<string>(() => {
if (type.value === 'time') return '选择具体时间';
if (type.value === 'duration') return 'ISO 8601格式如PT1H';
if (type.value === 'cycle') return 'CRON表达式或ISO 8601周期';
return '';
});
const helpHtml = computed<string>(() => {
if (type.value === 'duration') {
return `指定定时器之前要等待多长时间。S表示秒M表示分D表示天P表示时间段T表示精确到时间的时间段。<br>
时间格式依然为ISO 8601格式一年两个月三天四小时五分六秒内可以写成P1Y2M3DT4H5M6S<br>
P是开始标记T是时间和日期分割标记没有日期只有时间T是不能省去的比如1小时执行一次应写成PT1H`;
}
if (type.value === 'cycle') {
return `支持CRON表达式如0 0/30 * * * ?或ISO 8601周期如R3/PT10M`;
}
return '';
});
//
function syncFromBusinessObject(): void {
if (props.businessObject) {
const timerDef = (props.businessObject.eventDefinitions || [])[0];
if (timerDef) {
if (timerDef.timeDate) {
type.value = 'time';
condition.value = timerDef.timeDate.body;
} else if (timerDef.timeDuration) {
type.value = 'duration';
condition.value = timerDef.timeDuration.body;
} else if (timerDef.timeCycle) {
type.value = 'cycle';
condition.value = timerDef.timeCycle.body;
}
}
}
}
onMounted(syncFromBusinessObject);
//
function setType(t: string) {
type.value = t;
condition.value = '';
updateNode();
}
//
watch([type, condition], () => {
valid.value = validate();
// updateNode() //
});
function validate(): boolean {
if (type.value === 'time') {
return !!condition.value && !Number.isNaN(Date.parse(condition.value));
}
if (type.value === 'duration') {
return /^P.*$/.test(condition.value);
}
if (type.value === 'cycle') {
return /^(?:[0-9*/?, ]+|R\d*\/P.*)$/.test(condition.value);
}
return true;
}
//
function onDateChange(val: any) {
dateValue.value = val;
}
function onDateConfirm(): void {
if (dateValue.value) {
condition.value = new Date(dateValue.value).toISOString();
showDatePicker.value = false;
updateNode();
}
}
//
function onDurationChange(val: string) {
condition.value = val;
}
function onDurationConfirm(): void {
showDurationDialog.value = false;
updateNode();
}
//
function onCycleChange(val: string) {
condition.value = val;
}
function onCycleConfirm(): void {
showCycleDialog.value = false;
updateNode();
}
//
function handleInputFocus(): void {
if (type.value === 'time') showDatePicker.value = true;
if (type.value === 'duration') showDurationDialog.value = true;
if (type.value === 'cycle') showCycleDialog.value = true;
}
//
function updateNode(): void {
const moddle = (window.bpmnInstances as any)?.moddle;
const modeling = (window.bpmnInstances as any)?.modeling;
const elementRegistry = (window.bpmnInstances as any)?.elementRegistry;
if (!moddle || !modeling || !elementRegistry) return;
//
if (!props.businessObject || !props.businessObject.id) return;
const element = elementRegistry.get(props.businessObject.id);
if (!element) return;
// 1. timerDef
let timerDef =
element.businessObject.eventDefinitions &&
element.businessObject.eventDefinitions[0];
if (!timerDef) {
timerDef = bpmnInstances().bpmnFactory.create(
'bpmn:TimerEventDefinition',
{},
);
modeling.updateProperties(element, {
eventDefinitions: [timerDef],
});
}
// 2.
delete timerDef.timeDate;
delete timerDef.timeDuration;
delete timerDef.timeCycle;
// 3.
if (type.value === 'time' && condition.value) {
timerDef.timeDate = bpmnInstances().bpmnFactory.create(
'bpmn:FormalExpression',
{
body: condition.value,
},
);
} else if (type.value === 'duration' && condition.value) {
timerDef.timeDuration = bpmnInstances().bpmnFactory.create(
'bpmn:FormalExpression',
{
body: condition.value,
},
);
} else if (type.value === 'cycle' && condition.value) {
timerDef.timeCycle = bpmnInstances().bpmnFactory.create(
'bpmn:FormalExpression',
{
body: condition.value,
},
);
}
bpmnInstances().modeling.updateProperties(toRaw(element), {
eventDefinitions: [timerDef],
});
}
watch(
() => props.businessObject,
(val) => {
if (val) {
nextTick(() => {
syncFromBusinessObject();
});
}
},
{ immediate: true },
);
</script>
<template>
<div class="panel-tab__content">
<div style="margin-top: 10px">
<span>类型</span>
<Button.Group>
<Button
size="small"
:type="type === 'time' ? 'primary' : 'default'"
@click="setType('time')"
>
时间
</Button>
<Button
size="small"
:type="type === 'duration' ? 'primary' : 'default'"
@click="setType('duration')"
>
持续
</Button>
<Button
size="small"
:type="type === 'cycle' ? 'primary' : 'default'"
@click="setType('cycle')"
>
循环
</Button>
</Button.Group>
<CheckCircleFilled v-if="valid" style="color: green; margin-left: 8px" />
</div>
<div style="display: flex; align-items: center; margin-top: 10px">
<span>条件</span>
<Input
v-model:value="condition"
:placeholder="placeholder"
style="width: calc(100% - 100px)"
:readonly="type !== 'duration' && type !== 'cycle'"
@focus="handleInputFocus"
@blur="updateNode"
>
<template #suffix>
<Tooltip v-if="!valid" title="格式错误" placement="top">
<ExclamationCircleFilled style="color: orange" />
</Tooltip>
<Tooltip :title="helpText" placement="top">
<QuestionCircleFilled
style="color: #409eff; cursor: pointer"
@click="showHelp = true"
/>
</Tooltip>
<Button
v-if="type === 'time'"
@click="showDatePicker = true"
style="margin-left: 4px"
shape="circle"
size="small"
>
<IconifyIcon icon="ep:calendar" />
</Button>
<Button
v-if="type === 'duration'"
@click="showDurationDialog = true"
style="margin-left: 4px"
shape="circle"
size="small"
>
<IconifyIcon icon="ep:timer" />
</Button>
<Button
v-if="type === 'cycle'"
@click="showCycleDialog = true"
style="margin-left: 4px"
shape="circle"
size="small"
>
<IconifyIcon icon="ep:setting" />
</Button>
</template>
</Input>
</div>
<!-- 时间选择器 -->
<Modal
v-model:open="showDatePicker"
title="选择时间"
width="400px"
@cancel="showDatePicker = false"
>
<DatePicker
v-model:value="dateValue"
show-time
placeholder="选择日期时间"
style="width: 100%"
@change="onDateChange"
/>
<template #footer>
<Button @click="showDatePicker = false">取消</Button>
<Button type="primary" @click="onDateConfirm"></Button>
</template>
</Modal>
<!-- 持续时长选择器 -->
<Modal
v-model:open="showDurationDialog"
title="时间配置"
width="600px"
@cancel="showDurationDialog = false"
>
<DurationConfig :value="condition" @change="onDurationChange" />
<template #footer>
<Button @click="showDurationDialog = false">取消</Button>
<Button type="primary" @click="onDurationConfirm"></Button>
</template>
</Modal>
<!-- 循环配置器 -->
<Modal
v-model:open="showCycleDialog"
title="时间配置"
width="800px"
@cancel="showCycleDialog = false"
>
<CycleConfig :value="condition" @change="onCycleChange" />
<template #footer>
<Button @click="showCycleDialog = false">取消</Button>
<Button type="primary" @click="onCycleConfirm"></Button>
</template>
</Modal>
<!-- 帮助说明 -->
<Modal
v-model:open="showHelp"
title="格式说明"
width="600px"
@cancel="showHelp = false"
>
<div v-html="helpHtml"></div>
<template #footer>
<Button @click="showHelp = false">关闭</Button>
</template>
</Modal>
</div>
</template>
<style scoped>
/* 相关样式 */
</style>

View File

@ -0,0 +1,120 @@
@use './process-designer';
@use './process-panel';
$success-color: #4eb819;
$primary-color: #409eff;
$danger-color: #f56c6c;
$cancel-color: #909399;
.process-viewer {
position: relative;
background: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNDAiIGhlaWdodD0iNDAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGRlZnM+PHBhdHRlcm4gaWQ9ImEiIHdpZHRoPSI0MCIgaGVpZ2h0PSI0MCIgcGF0dGVyblVuaXRzPSJ1c2VyU3BhY2VPblVzZSI+PHBhdGggZD0iTTAgMTBoNDBNMTAgMHY0ME0wIDIwaDQwTTIwIDB2NDBNMCAzMGg0ME0zMCAwdjQwIiBmaWxsPSJub25lIiBzdHJva2U9IiNlMGUwZTAiIG9wYWNpdHk9Ii4yIi8+PHBhdGggZD0iTTQwIDBIMHY0MCIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjZTBlMGUwIi8+PC9wYXR0ZXJuPjwvZGVmcz48cmVjdCB3aWR0aD0iMTAwJSIgaGVpZ2h0PSIxMDAlIiBmaWxsPSJ1cmwoI2EpIi8+PC9zdmc+')
repeat !important;
border: 1px solid #efefef;
.success-arrow {
fill: $success-color;
stroke: $success-color;
}
.success-conditional {
fill: white;
stroke: $success-color;
}
.success.djs-connection {
.djs-visual path {
stroke: $success-color !important;
//marker-end: url(#sequenceflow-end-white-success)!important;
}
}
.success.djs-connection.condition-expression {
.djs-visual path {
//marker-start: url(#conditional-flow-marker-white-success)!important;
}
}
.success.djs-shape {
.djs-visual rect {
fill: $success-color !important;
fill-opacity: 0.15 !important;
stroke: $success-color !important;
}
.djs-visual polygon {
stroke: $success-color !important;
}
.djs-visual path:nth-child(2) {
fill: $success-color !important;
stroke: $success-color !important;
}
.djs-visual circle {
fill: $success-color !important;
fill-opacity: 0.15 !important;
stroke: $success-color !important;
}
}
.primary.djs-shape {
.djs-visual rect {
fill: $primary-color !important;
fill-opacity: 0.15 !important;
stroke: $primary-color !important;
}
.djs-visual polygon {
stroke: $primary-color !important;
}
.djs-visual circle {
fill: $primary-color !important;
fill-opacity: 0.15 !important;
stroke: $primary-color !important;
}
}
.danger.djs-shape {
.djs-visual rect {
fill: $danger-color !important;
fill-opacity: 0.15 !important;
stroke: $danger-color !important;
}
.djs-visual polygon {
stroke: $danger-color !important;
}
.djs-visual circle {
fill: $danger-color !important;
fill-opacity: 0.15 !important;
stroke: $danger-color !important;
}
}
.cancel.djs-shape {
.djs-visual rect {
fill: $cancel-color !important;
fill-opacity: 0.15 !important;
stroke: $cancel-color !important;
}
.djs-visual polygon {
stroke: $cancel-color !important;
}
.djs-visual circle {
fill: $cancel-color !important;
fill-opacity: 0.15 !important;
stroke: $cancel-color !important;
}
}
}
.process-viewer .djs-tooltip-container,
.process-viewer .djs-overlay-container,
.process-viewer .djs-palette {
display: none;
}

View File

@ -0,0 +1,184 @@
@use 'bpmn-js-token-simulation/assets/css/bpmn-js-token-simulation.css';
// token-simulation
.djs-palette {
background: var(--palette-background-color);
border: solid 1px var(--palette-border-color) !important;
border-radius: 2px;
}
.my-process-designer {
box-sizing: border-box;
display: flex;
flex-direction: column;
width: 100%;
height: 100%;
.my-process-designer__header {
width: 100%;
min-height: 36px;
.el-button {
text-align: center;
}
.el-button-group {
margin: 4px;
}
.el-tooltip__popper {
.el-button {
width: 100%;
padding-right: 8px;
padding-left: 8px;
text-align: left;
}
.el-button:hover {
color: #fff;
background: rgb(64 158 255 / 80%);
}
}
.align {
position: relative;
i {
&::after {
position: absolute;
content: '|';
// transform: rotate(90deg) translate(200%, 60%);
transform: rotate(180deg) translate(271%, -10%);
}
}
}
.align.align-left i {
transform: rotate(90deg);
}
.align.align-right i {
transform: rotate(-90deg);
}
.align.align-top i {
transform: rotate(180deg);
}
.align.align-bottom i {
transform: rotate(0deg);
}
.align.align-center i {
transform: rotate(0deg);
&::after {
// transform: rotate(90deg) translate(0, 60%);
transform: rotate(0deg) translate(-0%, -5%);
}
}
.align.align-middle i {
transform: rotate(-90deg);
&::after {
// transform: rotate(90deg) translate(0, 60%);
transform: rotate(0deg) translate(0, -10%);
}
}
}
.my-process-designer__container {
display: inline-flex;
flex: 1;
width: 100%;
.my-process-designer__canvas {
position: relative;
flex: 1;
height: 100%;
background: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNDAiIGhlaWdodD0iNDAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGRlZnM+PHBhdHRlcm4gaWQ9ImEiIHdpZHRoPSI0MCIgaGVpZ2h0PSI0MCIgcGF0dGVyblVuaXRzPSJ1c2VyU3BhY2VPblVzZSI+PHBhdGggZD0iTTAgMTBoNDBNMTAgMHY0ME0wIDIwaDQwTTIwIDB2NDBNMCAzMGg0ME0zMCAwdjQwIiBmaWxsPSJub25lIiBzdHJva2U9IiNlMGUwZTAiIG9wYWNpdHk9Ii4yIi8+PHBhdGggZD0iTTQwIDBIMHY0MCIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjZTBlMGUwIi8+PC9wYXR0ZXJuPjwvZGVmcz48cmVjdCB3aWR0aD0iMTAwJSIgaGVpZ2h0PSIxMDAlIiBmaWxsPSJ1cmwoI2EpIi8+PC9zdmc+')
repeat !important;
div.toggle-mode {
display: none;
}
}
.my-process-designer__property-panel {
z-index: 10;
height: 100%;
overflow: scroll;
overflow-y: auto;
* {
box-sizing: border-box;
}
}
// svg {
// width: 100%;
// height: 100%;
// min-height: 100%;
// overflow: hidden;
// }
}
}
//
// .djs-palette .two-column .open {
.open {
// .djs-palette.open {
.djs-palette-entries {
div[class^='bpmn-icon-']::before,
div[class*='bpmn-icon-']::before {
line-height: unset;
}
div.entry {
position: relative;
}
div.entry:hover {
&::after {
position: absolute;
top: 0;
right: -10px;
bottom: 0;
z-index: 100;
box-sizing: border-box;
display: inline-block;
width: max-content;
padding: 0 16px;
overflow: hidden;
font-size: 0.5em;
font-variant: normal;
vertical-align: text-bottom;
text-transform: none;
text-decoration: inherit;
content: attr(title);
background: #fafafa;
border: 1px solid #ccc;
border-radius: 4px;
box-shadow: 0 0 6px #eee;
transform: translateX(100%);
}
}
}
}
pre {
height: 100%;
max-height: calc(80vh - 32px);
margin: 0;
overflow: hidden;
overflow-y: auto;
}
.hljs {
word-break: break-word;
white-space: pre-wrap;
}
.hljs * {
font-family: Consolas, Monaco, monospace;
}

View File

@ -0,0 +1,127 @@
.process-panel__container {
box-sizing: border-box;
max-height: 100%;
padding: 0 8px;
overflow-y: scroll;
border-left: 1px solid #eee;
box-shadow: 0 0 8px #ccc;
}
.panel-tab__title {
padding: 0 8px;
font-size: 1.1em;
font-weight: 600;
line-height: 1.2em;
i {
margin-right: 8px;
font-size: 1.2em;
}
}
.panel-tab__content {
box-sizing: border-box;
width: 100%;
padding: 8px 16px;
border-top: 1px solid #eee;
.panel-tab__content--title {
display: flex;
justify-content: space-between;
padding-bottom: 8px;
span {
flex: 1;
text-align: left;
}
}
}
.element-property {
display: flex;
align-items: flex-start;
width: 100%;
margin: 8px 0;
.element-property__label {
box-sizing: border-box;
display: block;
width: 90px;
padding-right: 12px;
overflow: hidden;
font-size: 14px;
line-height: 32px;
text-align: right;
}
.element-property__value {
flex: 1;
line-height: 32px;
}
.el-form-item {
width: 100%;
padding-bottom: 18px;
margin-bottom: 0;
}
}
.list-property {
flex-direction: column;
.element-listener-item {
display: inline-grid;
grid-template-columns: 16px auto 32px 32px;
grid-column-gap: 8px;
width: 100%;
}
.element-listener-item + .element-listener-item {
margin-top: 8px;
}
}
.listener-filed__title {
display: inline-flex;
align-items: center;
justify-content: space-between;
width: 100%;
margin-top: 0;
span {
width: 200px;
font-size: 14px;
text-align: left;
}
i {
margin-right: 8px;
}
}
.element-drawer__button {
display: inline-flex;
justify-content: space-around;
width: 100%;
margin-top: 8px;
}
.element-drawer__button > .el-button {
width: 100%;
}
.el-collapse-item__content {
padding-bottom: 0;
}
.el-input.is-disabled .el-input__inner {
color: #999;
}
.el-form-item.el-form-item--mini {
margin-bottom: 0;
& + .el-form-item {
margin-top: 16px;
}
}

View File

@ -0,0 +1,93 @@
import { toRaw } from 'vue';
const bpmnInstances = () => (window as any)?.bpmnInstances;
// 创建监听器实例
export function createListenerObject(options, isTask, prefix) {
const listenerObj = Object.create(null);
listenerObj.event = options.event;
isTask && (listenerObj.id = options.id); // 任务监听器特有的 id 字段
switch (options.listenerType) {
case 'delegateExpressionListener': {
listenerObj.delegateExpression = options.delegateExpression;
break;
}
case 'expressionListener': {
listenerObj.expression = options.expression;
break;
}
case 'scriptListener': {
listenerObj.script = createScriptObject(options, prefix);
break;
}
default: {
listenerObj.class = options.class;
}
}
// 注入字段
if (options.fields) {
listenerObj.fields = options.fields.map((field) => {
return createFieldObject(field, prefix);
});
}
// 任务监听器的 定时器 设置
if (isTask && options.event === 'timeout' && !!options.eventDefinitionType) {
const timeDefinition = bpmnInstances().moddle.create(
'bpmn:FormalExpression',
{
body: options.eventTimeDefinitions,
},
);
const TimerEventDefinition = bpmnInstances().moddle.create(
'bpmn:TimerEventDefinition',
{
id: `TimerEventDefinition_${uuid(8)}`,
[`time${options.eventDefinitionType.replace(/^\S/, (s) => s.toUpperCase())}`]:
timeDefinition,
},
);
listenerObj.eventDefinitions = [TimerEventDefinition];
}
return bpmnInstances().moddle.create(
`${prefix}:${isTask ? 'TaskListener' : 'ExecutionListener'}`,
listenerObj,
);
}
// 创建 监听器的注入字段 实例
export function createFieldObject(option, prefix) {
const { name, fieldType, string, expression } = option;
const fieldConfig =
fieldType === 'string' ? { name, string } : { name, expression };
return bpmnInstances().moddle.create(`${prefix}:Field`, fieldConfig);
}
// 创建脚本实例
export function createScriptObject(options, prefix) {
const { scriptType, scriptFormat, value, resource } = options;
const scriptConfig =
scriptType === 'inlineScript'
? { scriptFormat, value }
: { scriptFormat, resource };
return bpmnInstances().moddle.create(`${prefix}:Script`, scriptConfig);
}
// 更新元素扩展属性
export function updateElementExtensions(element, extensionList) {
const extensions = bpmnInstances().moddle.create('bpmn:ExtensionElements', {
values: extensionList,
});
bpmnInstances().modeling.updateProperties(toRaw(element), {
extensionElements: extensions,
});
}
// 创建一个id
export function uuid(length = 8, chars?) {
let result = '';
const charsString =
chars || '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
for (let i = length; i > 0; --i) {
result += charsString[Math.floor(Math.random() * charsString.length)];
}
return result;
}

View File

@ -0,0 +1,5 @@
const hljs = require('highlight.js/lib/core');
hljs.registerLanguage('xml', require('highlight.js/lib/languages/xml'));
hljs.registerLanguage('json', require('highlight.js/lib/languages/json'));
module.exports = hljs;

Some files were not shown because too many files have changed in this diff Show More