feat(process-designer): [process-designer] add BPMN process designer component

Wraps bpmn-js to provide a BPMN 2.0 diagram editor following the
renderless / vue / theme three-layer architecture. Supports XML / SVG
import & export, zoom, undo / redo, readonly mode (Viewer), and
forwards bpmn-js events. Closes opentiny/tiny-vue#115507.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
YuxuanFan 2026-04-28 04:19:50 -07:00
parent 2698d3dd2d
commit 2f02d20dc5
29 changed files with 1284 additions and 0 deletions

View File

@ -0,0 +1,258 @@
export default {
mode: ['pc'],
apis: [
{
name: 'process-designer',
type: 'component',
props: [
{
name: 'value',
type: 'string',
defaultValue: "''",
desc: {
'zh-CN': 'BPMN 2.0 XML 字符串,作为流程图的初始内容;变更时会自动重新导入',
'en-US': 'BPMN 2.0 XML string used as the initial diagram. Re-imported automatically when changed.'
},
mode: ['pc'],
pcDemo: 'basic-usage'
},
{
name: 'xml',
type: 'string',
defaultValue: "''",
desc: {
'zh-CN': '与 value 等价,便于按属性命名习惯使用',
'en-US': 'Alias of value for naming convenience.'
},
mode: ['pc']
},
{
name: 'readonly',
type: 'boolean',
defaultValue: 'false',
desc: {
'zh-CN': '只读模式,不渲染编辑工具栏,底层使用 bpmn-js Viewer',
'en-US': 'Readonly mode. Toolbar is hidden and the underlying bpmn-js Viewer is used.'
},
mode: ['pc'],
pcDemo: 'readonly'
},
{
name: 'show-toolbar',
type: 'boolean',
defaultValue: 'true',
desc: {
'zh-CN': '是否显示内置工具栏(撤销 / 重做 / 缩放 / 导出)',
'en-US': 'Whether to show the built-in toolbar (undo / redo / zoom / export).'
},
mode: ['pc']
},
{
name: 'height',
type: 'string | number',
defaultValue: '600',
desc: {
'zh-CN': '组件整体高度,数字将转为 px',
'en-US': 'Overall component height; numbers are converted to px.'
},
mode: ['pc']
},
{
name: 'file-name',
type: 'string',
defaultValue: "'diagram'",
desc: {
'zh-CN': '调用 downloadXML / downloadSVG 时使用的文件名(不含扩展名)',
'en-US': 'File name (without extension) used when calling downloadXML / downloadSVG.'
},
mode: ['pc']
},
{
name: 'options',
type: 'object',
defaultValue: '{}',
desc: {
'zh-CN': '透传给 bpmn-js Modeler / Viewer 构造函数的额外配置',
'en-US': 'Extra options forwarded to the bpmn-js Modeler / Viewer constructor.'
},
mode: ['pc']
}
],
events: [
{
name: 'ready',
desc: {
'zh-CN': '内部 modeler 初始化完成时触发,回调参数为 modeler 实例',
'en-US': 'Fired when the underlying modeler is ready. Receives the modeler instance.'
},
mode: ['pc']
},
{
name: 'import-done',
desc: {
'zh-CN': 'XML 导入完成时触发',
'en-US': 'Fired when XML import completes.'
},
mode: ['pc']
},
{
name: 'import-error',
desc: {
'zh-CN': 'XML 导入失败时触发',
'en-US': 'Fired when XML import fails.'
},
mode: ['pc']
},
{
name: 'save-xml',
desc: {
'zh-CN': '调用 saveXML 后触发,回调参数包含 xml 字段',
'en-US': 'Fired after saveXML is called. Payload contains the xml field.'
},
mode: ['pc']
},
{
name: 'save-svg',
desc: {
'zh-CN': '调用 saveSVG 后触发,回调参数包含 svg 字段',
'en-US': 'Fired after saveSVG is called. Payload contains the svg field.'
},
mode: ['pc']
},
{
name: 'element.click',
desc: {
'zh-CN': '画布节点被点击时触发(透传 bpmn-js 事件)',
'en-US': 'Forwarded from bpmn-js when a diagram element is clicked.'
},
mode: ['pc'],
pcDemo: 'events'
},
{
name: 'selection.changed',
desc: {
'zh-CN': '选择变化时触发(透传 bpmn-js 事件)',
'en-US': 'Forwarded from bpmn-js when the selection changes.'
},
mode: ['pc'],
pcDemo: 'events'
}
],
methods: [
{
name: 'importXML',
args: 'xml: string',
returnType: 'Promise<{ warnings: any[] }>',
desc: {
'zh-CN': '导入指定的 BPMN XML 字符串',
'en-US': 'Import the given BPMN XML string.'
},
mode: ['pc']
},
{
name: 'saveXML',
args: 'options?: { format?: boolean }',
returnType: 'Promise<{ xml: string }>',
desc: {
'zh-CN': '导出当前流程图的 XML',
'en-US': 'Export the current diagram as XML.'
},
mode: ['pc']
},
{
name: 'saveSVG',
args: '',
returnType: 'Promise<{ svg: string }>',
desc: {
'zh-CN': '导出当前流程图的 SVG',
'en-US': 'Export the current diagram as SVG.'
},
mode: ['pc']
},
{
name: 'zoom',
args: "level: number | 'fit-viewport'",
desc: {
'zh-CN': '设置缩放级别,传入 fit-viewport 自适应画布',
'en-US': "Set zoom level. Pass 'fit-viewport' to fit the diagram to the canvas."
},
mode: ['pc']
},
{
name: 'zoomIn',
desc: {
'zh-CN': '在当前缩放基础上放大 10%',
'en-US': 'Zoom in by 10%.'
},
mode: ['pc']
},
{
name: 'zoomOut',
desc: {
'zh-CN': '在当前缩放基础上缩小 10%',
'en-US': 'Zoom out by 10%.'
},
mode: ['pc']
},
{
name: 'fitViewport',
desc: {
'zh-CN': '将流程图自适应到画布大小',
'en-US': 'Fit the diagram to the viewport.'
},
mode: ['pc']
},
{
name: 'undo',
desc: { 'zh-CN': '撤销上一步操作', 'en-US': 'Undo the last action.' },
mode: ['pc']
},
{
name: 'redo',
desc: { 'zh-CN': '重做上一步撤销', 'en-US': 'Redo the previous undo.' },
mode: ['pc']
},
{
name: 'clear',
desc: { 'zh-CN': '清空当前流程图', 'en-US': 'Clear the current diagram.' },
mode: ['pc']
},
{
name: 'downloadXML',
desc: {
'zh-CN': '将当前流程图作为 .bpmn 文件下载',
'en-US': 'Download the current diagram as a .bpmn file.'
},
mode: ['pc']
},
{
name: 'downloadSVG',
desc: {
'zh-CN': '将当前流程图作为 .svg 文件下载',
'en-US': 'Download the current diagram as a .svg file.'
},
mode: ['pc']
},
{
name: 'getModeler',
returnType: 'BpmnModeler | BpmnViewer | null',
desc: {
'zh-CN': '获取底层 bpmn-js 实例,便于扩展高级用法',
'en-US': 'Return the underlying bpmn-js instance for advanced usage.'
},
mode: ['pc']
}
],
slots: [
{
name: 'toolbar',
desc: {
'zh-CN': '自定义工具栏内容',
'en-US': 'Customize the toolbar content.'
},
mode: ['pc']
}
]
}
]
}

View File

@ -0,0 +1,44 @@
<template>
<tiny-process-designer :value="xml" :height="520" file-name="basic-diagram" @save-xml="onSaveXml" />
</template>
<script setup>
import { ref } from 'vue'
import { TinyProcessDesigner } from '@opentiny/vue'
const xml = ref(`<?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="basic" targetNamespace="http://bpmn.io/schema/bpmn">
<bpmn2:process id="Process_basic" isExecutable="false">
<bpmn2:startEvent id="StartEvent_1" name="开始"/>
<bpmn2:task id="Task_1" name="审批"/>
<bpmn2:endEvent id="EndEvent_1" name="结束"/>
<bpmn2:sequenceFlow id="Flow_1" sourceRef="StartEvent_1" targetRef="Task_1"/>
<bpmn2:sequenceFlow id="Flow_2" sourceRef="Task_1" targetRef="EndEvent_1"/>
</bpmn2:process>
<bpmndi:BPMNDiagram id="BPMNDiagram_1">
<bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="Process_basic">
<bpmndi:BPMNShape id="StartEvent_1_di" bpmnElement="StartEvent_1">
<dc:Bounds x="160" y="180" width="36" height="36"/>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Task_1_di" bpmnElement="Task_1">
<dc:Bounds x="260" y="158" width="100" height="80"/>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="EndEvent_1_di" bpmnElement="EndEvent_1">
<dc:Bounds x="430" y="180" width="36" height="36"/>
</bpmndi:BPMNShape>
<bpmndi:BPMNEdge id="Flow_1_di" bpmnElement="Flow_1">
<di:waypoint x="196" y="198"/>
<di:waypoint x="260" y="198"/>
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_2_di" bpmnElement="Flow_2">
<di:waypoint x="360" y="198"/>
<di:waypoint x="430" y="198"/>
</bpmndi:BPMNEdge>
</bpmndi:BPMNPlane>
</bpmndi:BPMNDiagram>
</bpmn2:definitions>`)
const onSaveXml = ({ xml }) => {
console.log('saved xml length:', xml && xml.length)
}
</script>

View File

@ -0,0 +1,18 @@
import { test, expect } from '@playwright/test'
test('process-designer 基本用法', async ({ page }) => {
page.on('pageerror', (exception) => expect(exception).toBeNull())
await page.goto('process-designer#basic-usage')
const wrap = page.locator('#basic-usage')
const designer = wrap.locator('.tiny-process-designer')
await expect(designer).toBeVisible()
// bpmn-js 渲染完成后画布内会出现 SVG
const svg = designer.locator('.tiny-process-designer__canvas svg').first()
await expect(svg).toBeVisible()
// 工具栏按钮存在
const toolbar = designer.locator('.tiny-process-designer__toolbar')
await expect(toolbar.locator('button')).toHaveCount(7)
})

View File

@ -0,0 +1,55 @@
<template>
<tiny-process-designer :value="xml" :height="520" file-name="basic-diagram" @save-xml="onSaveXml" />
</template>
<script>
import { TinyProcessDesigner } from '@opentiny/vue'
const initialXml = `<?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="basic" targetNamespace="http://bpmn.io/schema/bpmn">
<bpmn2:process id="Process_basic" isExecutable="false">
<bpmn2:startEvent id="StartEvent_1" name="开始"/>
<bpmn2:task id="Task_1" name="审批"/>
<bpmn2:endEvent id="EndEvent_1" name="结束"/>
<bpmn2:sequenceFlow id="Flow_1" sourceRef="StartEvent_1" targetRef="Task_1"/>
<bpmn2:sequenceFlow id="Flow_2" sourceRef="Task_1" targetRef="EndEvent_1"/>
</bpmn2:process>
<bpmndi:BPMNDiagram id="BPMNDiagram_1">
<bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="Process_basic">
<bpmndi:BPMNShape id="StartEvent_1_di" bpmnElement="StartEvent_1">
<dc:Bounds x="160" y="180" width="36" height="36"/>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Task_1_di" bpmnElement="Task_1">
<dc:Bounds x="260" y="158" width="100" height="80"/>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="EndEvent_1_di" bpmnElement="EndEvent_1">
<dc:Bounds x="430" y="180" width="36" height="36"/>
</bpmndi:BPMNShape>
<bpmndi:BPMNEdge id="Flow_1_di" bpmnElement="Flow_1">
<di:waypoint x="196" y="198"/>
<di:waypoint x="260" y="198"/>
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_2_di" bpmnElement="Flow_2">
<di:waypoint x="360" y="198"/>
<di:waypoint x="430" y="198"/>
</bpmndi:BPMNEdge>
</bpmndi:BPMNPlane>
</bpmndi:BPMNDiagram>
</bpmn2:definitions>`
export default {
components: {
TinyProcessDesigner
},
data() {
return {
xml: initialXml
}
},
methods: {
onSaveXml({ xml }) {
console.log('saved xml length:', xml && xml.length)
}
}
}
</script>

View File

@ -0,0 +1,21 @@
<template>
<div>
<tiny-process-designer :height="480" @element.click="onElementClick" @selection.changed="onSelectionChanged" />
<p>当前选中{{ selectedId || '无' }}</p>
</div>
</template>
<script setup>
import { ref } from 'vue'
import { TinyProcessDesigner } from '@opentiny/vue'
const selectedId = ref('')
const onElementClick = (event) => {
selectedId.value = event && event.element ? event.element.id : ''
}
const onSelectionChanged = ({ newSelection }) => {
selectedId.value = newSelection && newSelection[0] ? newSelection[0].id : ''
}
</script>

View File

@ -0,0 +1,11 @@
import { test, expect } from '@playwright/test'
test('process-designer 事件监听', async ({ page }) => {
page.on('pageerror', (exception) => expect(exception).toBeNull())
await page.goto('process-designer#events')
const wrap = page.locator('#events')
const designer = wrap.locator('.tiny-process-designer')
await expect(designer).toBeVisible()
await expect(designer.locator('.tiny-process-designer__canvas svg').first()).toBeVisible()
})

View File

@ -0,0 +1,32 @@
<template>
<div>
<tiny-process-designer
ref="designer"
:height="480"
@element.click="onElementClick"
@selection.changed="onSelectionChanged"
/>
<p>当前选中{{ selectedId || '无' }}</p>
</div>
</template>
<script>
import { TinyProcessDesigner } from '@opentiny/vue'
export default {
components: { TinyProcessDesigner },
data() {
return {
selectedId: ''
}
},
methods: {
onElementClick(event) {
this.selectedId = event && event.element ? event.element.id : ''
},
onSelectionChanged({ newSelection }) {
this.selectedId = newSelection && newSelection[0] ? newSelection[0].id : ''
}
}
}
</script>

View File

@ -0,0 +1,31 @@
<template>
<tiny-process-designer :value="xml" :height="420" readonly />
</template>
<script setup>
import { ref } from 'vue'
import { TinyProcessDesigner } from '@opentiny/vue'
const xml = ref(`<?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="readonly" targetNamespace="http://bpmn.io/schema/bpmn">
<bpmn2:process id="Process_ro" isExecutable="false">
<bpmn2:startEvent id="StartEvent_1"/>
<bpmn2:endEvent id="EndEvent_1"/>
<bpmn2:sequenceFlow id="Flow_1" sourceRef="StartEvent_1" targetRef="EndEvent_1"/>
</bpmn2:process>
<bpmndi:BPMNDiagram id="BPMNDiagram_1">
<bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="Process_ro">
<bpmndi:BPMNShape id="StartEvent_1_di" bpmnElement="StartEvent_1">
<dc:Bounds x="160" y="180" width="36" height="36"/>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="EndEvent_1_di" bpmnElement="EndEvent_1">
<dc:Bounds x="280" y="180" width="36" height="36"/>
</bpmndi:BPMNShape>
<bpmndi:BPMNEdge id="Flow_1_di" bpmnElement="Flow_1">
<di:waypoint x="196" y="198"/>
<di:waypoint x="280" y="198"/>
</bpmndi:BPMNEdge>
</bpmndi:BPMNPlane>
</bpmndi:BPMNDiagram>
</bpmn2:definitions>`)
</script>

View File

@ -0,0 +1,14 @@
import { test, expect } from '@playwright/test'
test('process-designer 只读模式', async ({ page }) => {
page.on('pageerror', (exception) => expect(exception).toBeNull())
await page.goto('process-designer#readonly')
const wrap = page.locator('#readonly')
const designer = wrap.locator('.tiny-process-designer')
await expect(designer).toBeVisible()
// 只读模式下不渲染工具栏
await expect(designer.locator('.tiny-process-designer__toolbar')).toHaveCount(0)
await expect(designer.locator('.tiny-process-designer__canvas svg').first()).toBeVisible()
})

View File

@ -0,0 +1,37 @@
<template>
<tiny-process-designer :value="xml" :height="420" readonly />
</template>
<script>
import { TinyProcessDesigner } from '@opentiny/vue'
export default {
components: { TinyProcessDesigner },
data() {
return {
xml: `<?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="readonly" targetNamespace="http://bpmn.io/schema/bpmn">
<bpmn2:process id="Process_ro" isExecutable="false">
<bpmn2:startEvent id="StartEvent_1"/>
<bpmn2:endEvent id="EndEvent_1"/>
<bpmn2:sequenceFlow id="Flow_1" sourceRef="StartEvent_1" targetRef="EndEvent_1"/>
</bpmn2:process>
<bpmndi:BPMNDiagram id="BPMNDiagram_1">
<bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="Process_ro">
<bpmndi:BPMNShape id="StartEvent_1_di" bpmnElement="StartEvent_1">
<dc:Bounds x="160" y="180" width="36" height="36"/>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="EndEvent_1_di" bpmnElement="EndEvent_1">
<dc:Bounds x="280" y="180" width="36" height="36"/>
</bpmndi:BPMNShape>
<bpmndi:BPMNEdge id="Flow_1_di" bpmnElement="Flow_1">
<di:waypoint x="196" y="198"/>
<di:waypoint x="280" y="198"/>
</bpmndi:BPMNEdge>
</bpmndi:BPMNPlane>
</bpmndi:BPMNDiagram>
</bpmn2:definitions>`
}
}
}
</script>

View File

@ -0,0 +1,7 @@
---
title: ProcessDesigner 流程设计器
---
# ProcessDesigner 流程设计器
基于 [bpmn-js](https://github.com/bpmn-io/bpmn-js) 封装的 BPMN 2.0 流程图设计器组件,支持在画布上图形化绘制业务流程,提供撤销 / 重做 / 缩放 / 导出 XML / 导出 SVG 等基础能力,可用于工作流配置、审批流编排等场景。

View File

@ -0,0 +1,7 @@
---
title: ProcessDesigner
---
# ProcessDesigner
A BPMN 2.0 process designer component built on top of [bpmn-js](https://github.com/bpmn-io/bpmn-js). It provides graphical authoring of business processes on a canvas, with built-in undo / redo / zoom and XML / SVG export — suitable for workflow configuration, approval-flow design, and similar scenarios.

View File

@ -0,0 +1,50 @@
export default {
column: '2',
owner: '',
show: true,
cloud: false,
demos: [
{
demoId: 'basic-usage',
name: {
'zh-CN': '基本用法',
'en-US': 'Basic Usage'
},
desc: {
'zh-CN':
'<p>通过 <code>value</code> 传入 BPMN 2.0 XML 即可渲染流程图,可在画布上拖拽编辑,使用工具栏完成撤销 / 重做 / 缩放 / 导出。</p>\n',
'en-US':
'<p>Pass BPMN 2.0 XML via <code>value</code> to render the diagram. The diagram can be edited on the canvas, with the toolbar offering undo / redo / zoom / export.</p>\n'
},
codeFiles: ['basic-usage.vue']
},
{
demoId: 'readonly',
name: {
'zh-CN': '只读模式',
'en-US': 'Readonly'
},
desc: {
'zh-CN':
'<p>设置 <code>readonly</code> 属性后只渲染流程图、不可编辑,适用于流程预览场景。底层使用 <code>bpmn-js</code> 的 <code>Viewer</code>。</p>\n',
'en-US':
'<p>With <code>readonly</code> set, the diagram is rendered without editing capabilities — suitable for read-only preview. Backed by <code>bpmn-js</code> <code>Viewer</code>.</p>\n'
},
codeFiles: ['readonly.vue']
},
{
demoId: 'events',
name: {
'zh-CN': '事件监听',
'en-US': 'Events'
},
desc: {
'zh-CN':
'<p>组件透传 <code>bpmn-js</code> 的常用事件,可监听 <code>element.click</code>、<code>selection.changed</code>、<code>commandStack.changed</code> 等。</p>\n',
'en-US':
'<p>Common <code>bpmn-js</code> events are forwarded, such as <code>element.click</code>, <code>selection.changed</code>, and <code>commandStack.changed</code>.</p>\n'
},
codeFiles: ['events.vue']
}
]
}

View File

@ -262,6 +262,7 @@ export const cmpMenus = [
{ 'nameCn': '图片', 'name': 'Image', 'key': 'image' }, { 'nameCn': '图片', 'name': 'Image', 'key': 'image' },
{ 'nameCn': '无限滚动', 'name': 'InfiniteScroll', 'key': 'infinite-scroll' }, { 'nameCn': '无限滚动', 'name': 'InfiniteScroll', 'key': 'infinite-scroll' },
{ 'nameCn': '里程碑', 'name': 'Milestone', 'key': 'milestone' }, { 'nameCn': '里程碑', 'name': 'Milestone', 'key': 'milestone' },
{ 'nameCn': '流程设计器', 'name': 'ProcessDesigner', 'key': 'process-designer' },
{ {
'nameCn': '思维导图', 'nameCn': '思维导图',
'name': 'MindMap', 'name': 'MindMap',

View File

@ -40,6 +40,14 @@
"@opentiny/vue-hooks": "workspace:~", "@opentiny/vue-hooks": "workspace:~",
"color": "4.2.3" "color": "4.2.3"
}, },
"peerDependencies": {
"bpmn-js": "^17.0.0"
},
"peerDependenciesMeta": {
"bpmn-js": {
"optional": true
}
},
"devDependencies": { "devDependencies": {
"esno": "^4.7.0", "esno": "^4.7.0",
"tsup": "7.2.0" "tsup": "7.2.0"

View File

@ -0,0 +1,200 @@
/**
* Copyright (c) 2022 - present TinyVue Authors.
* Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd.
*
* Use of this source code is governed by an MIT-style license.
*
* THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL,
* BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR
* A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS.
*
*/
export const EMPTY_BPMN_XML = `<?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="sample-diagram" targetNamespace="http://bpmn.io/schema/bpmn">
<bpmn2:process id="Process_1" isExecutable="false">
<bpmn2:startEvent id="StartEvent_1"/>
</bpmn2:process>
<bpmndi:BPMNDiagram id="BPMNDiagram_1">
<bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="Process_1">
<bpmndi:BPMNShape id="_BPMNShape_StartEvent_2" bpmnElement="StartEvent_1">
<dc:Bounds height="36.0" width="36.0" x="412.0" y="240.0"/>
</bpmndi:BPMNShape>
</bpmndi:BPMNPlane>
</bpmndi:BPMNDiagram>
</bpmn2:definitions>`
const FORWARDED_EVENTS = [
'element.click',
'element.dblclick',
'element.hover',
'element.out',
'element.changed',
'shape.added',
'shape.removed',
'connection.added',
'connection.removed',
'commandStack.changed',
'selection.changed',
'import.done',
'saveXML.done'
] as const
export const createModeler =
({ state, props, emit, api }) =>
async () => {
const BpmnLib = props.readonly ? await import('bpmn-js/lib/Viewer') : await import('bpmn-js/lib/Modeler')
const Ctor = BpmnLib.default
const modeler = new Ctor({
container: state.canvasRef,
keyboard: { bindTo: document },
...(props.options || {})
})
state.modeler = modeler
FORWARDED_EVENTS.forEach((event) => {
modeler.on(event, (payload) => {
emit(event, payload, modeler)
})
})
modeler.on('commandStack.changed', () => {
const stack = modeler.get('commandStack')
state.canUndo = stack.canUndo()
state.canRedo = stack.canRedo()
})
const xml = props.value || props.xml || EMPTY_BPMN_XML
await api.importXML(xml)
emit('ready', modeler)
return modeler
}
export const importXML =
({ state, emit }) =>
async (xml: string) => {
if (!state.modeler) return { warnings: [] }
try {
const result = await state.modeler.importXML(xml || EMPTY_BPMN_XML)
state.lastError = null
emit('import-done', result)
return result
} catch (err) {
state.lastError = err
emit('import-error', err)
throw err
}
}
export const saveXML =
({ state, emit }) =>
async (options?: { format?: boolean }) => {
if (!state.modeler) return { xml: '' }
const result = await state.modeler.saveXML({ format: true, ...(options || {}) })
emit('save-xml', result)
return result
}
export const saveSVG =
({ state, emit }) =>
async () => {
if (!state.modeler) return { svg: '' }
const result = await state.modeler.saveSVG()
emit('save-svg', result)
return result
}
export const zoom =
({ state }) =>
(level: number | 'fit-viewport') => {
if (!state.modeler) return
state.modeler.get('canvas').zoom(level)
}
export const zoomIn =
({ state, api }) =>
() => {
if (!state.modeler) return
const canvas = state.modeler.get('canvas')
const current = canvas.zoom()
api.zoom(Math.min(current + 0.1, 4))
}
export const zoomOut =
({ state, api }) =>
() => {
if (!state.modeler) return
const canvas = state.modeler.get('canvas')
const current = canvas.zoom()
api.zoom(Math.max(current - 0.1, 0.2))
}
export const fitViewport =
({ api }) =>
() => {
api.zoom('fit-viewport')
}
export const undo =
({ state }) =>
() => {
if (!state.modeler) return
state.modeler.get('commandStack').undo()
}
export const redo =
({ state }) =>
() => {
if (!state.modeler) return
state.modeler.get('commandStack').redo()
}
export const clear =
({ api }) =>
() =>
api.importXML(EMPTY_BPMN_XML)
export const downloadFile = (filename: string, content: string, mime: string) => {
if (typeof document === 'undefined') return
const blob = new Blob([content], { type: mime })
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = filename
document.body.appendChild(a)
a.click()
document.body.removeChild(a)
URL.revokeObjectURL(url)
}
export const downloadXML =
({ api, props }) =>
async () => {
const { xml } = await api.saveXML({ format: true })
if (xml) downloadFile(`${props.fileName || 'diagram'}.bpmn`, xml, 'application/xml')
}
export const downloadSVG =
({ api, props }) =>
async () => {
const { svg } = await api.saveSVG()
if (svg) downloadFile(`${props.fileName || 'diagram'}.svg`, svg, 'image/svg+xml')
}
export const getModeler =
({ state }) =>
() =>
state.modeler
export const destroy =
({ state }) =>
() => {
if (state.modeler) {
state.modeler.destroy()
state.modeler = null
}
}

View File

@ -0,0 +1,119 @@
/**
* Copyright (c) 2022 - present TinyVue Authors.
* Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd.
*
* Use of this source code is governed by an MIT-style license.
*
* THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL,
* BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR
* A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS.
*
*/
import {
createModeler,
importXML,
saveXML,
saveSVG,
zoom,
zoomIn,
zoomOut,
fitViewport,
undo,
redo,
clear,
downloadXML,
downloadSVG,
getModeler,
destroy
} from './index'
export const api = [
'state',
'importXML',
'saveXML',
'saveSVG',
'zoom',
'zoomIn',
'zoomOut',
'fitViewport',
'undo',
'redo',
'clear',
'downloadXML',
'downloadSVG',
'getModeler'
]
const initState = ({ reactive, props }) => {
const state = reactive({
modeler: null,
canvasRef: null,
canUndo: false,
canRedo: false,
lastError: null,
readonly: props.readonly
})
return state
}
const initApi = ({ api, state, props, emit }) => {
Object.assign(api, {
state,
createModeler: createModeler({ state, props, emit, api }),
importXML: importXML({ state, emit }),
saveXML: saveXML({ state, emit }),
saveSVG: saveSVG({ state, emit }),
zoom: zoom({ state }),
zoomIn: zoomIn({ state, api }),
zoomOut: zoomOut({ state, api }),
fitViewport: fitViewport({ api }),
undo: undo({ state }),
redo: redo({ state }),
clear: clear({ api }),
downloadXML: downloadXML({ api, props }),
downloadSVG: downloadSVG({ api, props }),
getModeler: getModeler({ state }),
destroy: destroy({ state })
})
}
const initWatch = ({ watch, state, api, props }) => {
watch(
() => props.value,
(val) => {
if (state.modeler && typeof val === 'string') {
api.importXML(val)
}
}
)
watch(
() => props.xml,
(val) => {
if (state.modeler && typeof val === 'string') {
api.importXML(val)
}
}
)
}
export const renderless = (props, { onMounted, onBeforeUnmount, reactive, watch }, { emit, vm }) => {
const api = {}
const state = initState({ reactive, props })
initApi({ api, state, props, emit })
initWatch({ watch, state, api, props })
onMounted(() => {
state.canvasRef = vm.$refs.canvas
api.createModeler()
})
onBeforeUnmount(() => {
api.destroy()
})
return api
}

View File

@ -101,6 +101,7 @@
@import './popeditor/index.less'; @import './popeditor/index.less';
@import './popover/index.less'; @import './popover/index.less';
@import './popup/index.less'; @import './popup/index.less';
@import './process-designer/index.less';
@import './progress/index.less'; @import './progress/index.less';
@import './qr-code/index.less'; @import './qr-code/index.less';
@import './quarter-panel/index.less'; @import './quarter-panel/index.less';

View File

@ -0,0 +1,96 @@
/**
* Copyright (c) 2022 - present TinyVue Authors.
* Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd.
*
* Use of this source code is governed by an MIT-style license.
*
* THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL,
* BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR
* A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS.
*
*/
@import '../mixins/common.less';
@import '../custom.less';
@import './vars.less';
@process-designer-prefix-cls: ~'@{css-prefix}process-designer';
.@{process-designer-prefix-cls} {
.inject-ProcessDesigner-vars();
position: relative;
display: flex;
flex-direction: column;
width: 100%;
border: 1px solid var(--tv-ProcessDesigner-border-color);
border-radius: var(--tv-ProcessDesigner-border-radius);
overflow: hidden;
box-sizing: border-box;
&__toolbar {
display: flex;
align-items: center;
flex-wrap: wrap;
flex: 0 0 auto;
height: var(--tv-ProcessDesigner-toolbar-height);
padding: var(--tv-ProcessDesigner-toolbar-padding);
background-color: var(--tv-ProcessDesigner-toolbar-bg-color);
border-bottom: 1px solid var(--tv-ProcessDesigner-toolbar-border-color);
gap: 4px;
box-sizing: border-box;
}
&__btn {
display: inline-flex;
align-items: center;
justify-content: center;
height: var(--tv-ProcessDesigner-btn-size);
min-width: var(--tv-ProcessDesigner-btn-size);
padding: 0 6px;
background: transparent;
border: none;
border-radius: var(--tv-ProcessDesigner-border-radius);
color: var(--tv-ProcessDesigner-btn-text-color);
cursor: pointer;
font-size: 14px;
line-height: 1;
.user-select(none);
&:hover:not(:disabled) {
background-color: var(--tv-ProcessDesigner-btn-hover-bg-color);
}
&:disabled {
color: var(--tv-ProcessDesigner-btn-disabled-text-color);
cursor: not-allowed;
}
.tiny-svg-size {
width: 16px;
height: 16px;
}
}
&__btn-label {
margin-left: 4px;
font-size: 12px;
}
&__divider {
display: inline-block;
width: 1px;
height: 16px;
margin: 0 4px;
background-color: var(--tv-ProcessDesigner-divider-color);
}
&__canvas {
flex: 1 1 auto;
width: 100%;
height: 100%;
min-height: 0;
background-color: var(--tv-ProcessDesigner-canvas-bg-color);
overflow: hidden;
}
}

View File

@ -0,0 +1,38 @@
/**
* Copyright (c) 2022 - present TinyVue Authors.
* Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd.
*
* Use of this source code is governed by an MIT-style license.
*
* THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL,
* BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR
* A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS.
*
*/
.inject-ProcessDesigner-vars() {
// 整体边框颜色
--tv-ProcessDesigner-border-color: var(--tv-color-border, #dcdfe6);
// 整体圆角
--tv-ProcessDesigner-border-radius: var(--tv-border-radius-m, 4px);
// 画布背景色
--tv-ProcessDesigner-canvas-bg-color: var(--tv-color-bg-2, #f5f5f5);
// 工具栏背景色
--tv-ProcessDesigner-toolbar-bg-color: var(--tv-color-bg-1, #ffffff);
// 工具栏底部边框颜色
--tv-ProcessDesigner-toolbar-border-color: var(--tv-color-border-separator, #ebeef5);
// 工具栏高度
--tv-ProcessDesigner-toolbar-height: var(--tv-size-height-lg, 40px);
// 工具栏内边距
--tv-ProcessDesigner-toolbar-padding: 4px 8px;
// 按钮尺寸
--tv-ProcessDesigner-btn-size: 28px;
// 按钮文字颜色
--tv-ProcessDesigner-btn-text-color: var(--tv-color-text-primary, #252b3a);
// 按钮悬浮背景色
--tv-ProcessDesigner-btn-hover-bg-color: var(--tv-color-bg-hover, #f2f5fc);
// 按钮禁用文字颜色
--tv-ProcessDesigner-btn-disabled-text-color: var(--tv-color-text-disabled, #adb0b8);
// 分割线颜色
--tv-ProcessDesigner-divider-color: var(--tv-color-border-separator, #ebeef5);
}

View File

@ -479,6 +479,15 @@ export default {
hide: 'Hide Sidebar', hide: 'Hide Sidebar',
show: 'Show Sidebar' show: 'Show Sidebar'
}, },
processDesigner: {
undo: 'Undo',
redo: 'Redo',
zoomIn: 'Zoom In',
zoomOut: 'Zoom Out',
fitViewport: 'Fit Viewport',
downloadXml: 'Download XML',
downloadSvg: 'Download SVG'
},
navMenu: { navMenu: {
moreText: 'More', moreText: 'More',
navigation: 'Navigation', navigation: 'Navigation',

View File

@ -474,6 +474,15 @@ export default {
hide: '隐藏侧边栏', hide: '隐藏侧边栏',
show: '显示侧边栏' show: '显示侧边栏'
}, },
processDesigner: {
undo: '撤销',
redo: '重做',
zoomIn: '放大',
zoomOut: '缩小',
fitViewport: '适配画布',
downloadXml: '下载 XML',
downloadSvg: '下载 SVG'
},
navMenu: { navMenu: {
moreText: '更多', moreText: '更多',
navigation: '导航', navigation: '导航',

View File

@ -120,6 +120,7 @@ import Popconfirm from '@opentiny/vue-popconfirm'
import Popeditor from '@opentiny/vue-popeditor' import Popeditor from '@opentiny/vue-popeditor'
import Popover from '@opentiny/vue-popover' import Popover from '@opentiny/vue-popover'
import Popup from '@opentiny/vue-popup' import Popup from '@opentiny/vue-popup'
import ProcessDesigner from '@opentiny/vue-process-designer'
import Progress from '@opentiny/vue-progress' import Progress from '@opentiny/vue-progress'
import PullRefresh from '@opentiny/vue-pull-refresh' import PullRefresh from '@opentiny/vue-pull-refresh'
import QrCode from '@opentiny/vue-qr-code' import QrCode from '@opentiny/vue-qr-code'
@ -322,6 +323,7 @@ const components = [
Popeditor, Popeditor,
Popover, Popover,
Popup, Popup,
ProcessDesigner,
Progress, Progress,
PullRefresh, PullRefresh,
QrCode, QrCode,
@ -665,6 +667,8 @@ export {
Popover as TinyPopover, Popover as TinyPopover,
Popup, Popup,
Popup as TinyPopup, Popup as TinyPopup,
ProcessDesigner,
ProcessDesigner as TinyProcessDesigner,
Progress, Progress,
Progress as TinyProgress, Progress as TinyProgress,
PullRefresh, PullRefresh,
@ -1066,6 +1070,8 @@ export default {
TinyPopover: Popover, TinyPopover: Popover,
Popup, Popup,
TinyPopup: Popup, TinyPopup: Popup,
ProcessDesigner,
TinyProcessDesigner: ProcessDesigner,
Progress, Progress,
TinyProgress: Progress, TinyProgress: Progress,
PullRefresh, PullRefresh,

View File

@ -154,6 +154,7 @@
"@opentiny/vue-popeditor": "workspace:~", "@opentiny/vue-popeditor": "workspace:~",
"@opentiny/vue-popover": "workspace:~", "@opentiny/vue-popover": "workspace:~",
"@opentiny/vue-popup": "workspace:~", "@opentiny/vue-popup": "workspace:~",
"@opentiny/vue-process-designer": "workspace:~",
"@opentiny/vue-progress": "workspace:~", "@opentiny/vue-progress": "workspace:~",
"@opentiny/vue-pull-refresh": "workspace:~", "@opentiny/vue-pull-refresh": "workspace:~",
"@opentiny/vue-qr-code": "workspace:~", "@opentiny/vue-qr-code": "workspace:~",

View File

@ -106,6 +106,7 @@ import Popconfirm from '@opentiny/vue-popconfirm'
import Popeditor from '@opentiny/vue-popeditor' import Popeditor from '@opentiny/vue-popeditor'
import Popover from '@opentiny/vue-popover' import Popover from '@opentiny/vue-popover'
import Popup from '@opentiny/vue-popup' import Popup from '@opentiny/vue-popup'
import ProcessDesigner from '@opentiny/vue-process-designer'
import Progress from '@opentiny/vue-progress' import Progress from '@opentiny/vue-progress'
import QrCode from '@opentiny/vue-qr-code' import QrCode from '@opentiny/vue-qr-code'
import QuarterPanel from '@opentiny/vue-quarter-panel' import QuarterPanel from '@opentiny/vue-quarter-panel'
@ -282,6 +283,7 @@ const components = [
Popeditor, Popeditor,
Popover, Popover,
Popup, Popup,
ProcessDesigner,
Progress, Progress,
QrCode, QrCode,
QuarterPanel, QuarterPanel,
@ -588,6 +590,8 @@ export {
Popover as TinyPopover, Popover as TinyPopover,
Popup, Popup,
Popup as TinyPopup, Popup as TinyPopup,
ProcessDesigner,
ProcessDesigner as TinyProcessDesigner,
Progress, Progress,
Progress as TinyProgress, Progress as TinyProgress,
QrCode, QrCode,
@ -937,6 +941,8 @@ export default {
TinyPopover: Popover, TinyPopover: Popover,
Popup, Popup,
TinyPopup: Popup, TinyPopup: Popup,
ProcessDesigner,
TinyProcessDesigner: ProcessDesigner,
Progress, Progress,
TinyProgress: Progress, TinyProgress: Progress,
QrCode, QrCode,

View File

@ -0,0 +1,29 @@
/**
* Copyright (c) 2022 - present TinyVue Authors.
* Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd.
*
* Use of this source code is governed by an MIT-style license.
*
* THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL,
* BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR
* A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS.
*
*/
import ProcessDesigner from './src/index'
import { version } from './package.json'
/* istanbul ignore next */
ProcessDesigner.install = function (Vue) {
Vue.component(ProcessDesigner.name, ProcessDesigner)
}
ProcessDesigner.version = version
/* istanbul ignore next */
if (process.env.BUILD_TARGET === 'runtime') {
if (typeof window !== 'undefined' && window.Vue) {
ProcessDesigner.install(window.Vue)
}
}
export default ProcessDesigner

View File

@ -0,0 +1,25 @@
{
"name": "@opentiny/vue-process-designer",
"type": "module",
"version": "3.30.0",
"description": "",
"license": "MIT",
"sideEffects": false,
"main": "lib/index.js",
"module": "index.ts",
"scripts": {
"build": "pnpm -w build:ui $npm_package_name"
},
"dependencies": {
"@opentiny/utils": "workspace:~",
"@opentiny/vue-common": "workspace:~",
"@opentiny/vue-icon": "workspace:~",
"@opentiny/vue-renderless": "workspace:~",
"@opentiny/vue-theme": "workspace:~",
"bpmn-js": "^17.11.1"
},
"devDependencies": {
"@opentiny-internal/vue-test-utils": "workspace:*",
"vitest": "catalog:"
}
}

View File

@ -0,0 +1,51 @@
/**
* Copyright (c) 2022 - present TinyVue Authors.
* Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd.
*
* Use of this source code is governed by an MIT-style license.
*
* THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL,
* BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR
* A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS.
*
*/
import { $props, $prefix, $setup, defineComponent } from '@opentiny/vue-common'
import template from 'virtual-template?pc'
export default defineComponent({
name: $prefix + 'ProcessDesigner',
props: {
...$props,
value: {
type: String,
default: ''
},
xml: {
type: String,
default: ''
},
readonly: {
type: Boolean,
default: false
},
showToolbar: {
type: Boolean,
default: true
},
height: {
type: [String, Number],
default: 600
},
fileName: {
type: String,
default: 'diagram'
},
options: {
type: Object,
default: () => ({})
}
},
setup(props, context) {
return $setup({ props, context, template })
}
})

View File

@ -0,0 +1,100 @@
<!--
* Copyright (c) 2022 - present TinyVue Authors.
* Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd.
*
* Use of this source code is governed by an MIT-style license.
*
* THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL,
* BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR
* A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS.
*
-->
<template>
<div class="tiny-process-designer" :style="{ height: typeof height === 'number' ? height + 'px' : height }">
<div v-if="showToolbar && !readonly" class="tiny-process-designer__toolbar">
<slot name="toolbar" :api="$attrs">
<button
class="tiny-process-designer__btn"
:disabled="!state.canUndo"
:title="t('ui.processDesigner.undo')"
@click="undo"
>
<icon-undo class="tiny-svg-size" />
</button>
<button
class="tiny-process-designer__btn"
:disabled="!state.canRedo"
:title="t('ui.processDesigner.redo')"
@click="redo"
>
<icon-redo class="tiny-svg-size" />
</button>
<i class="tiny-process-designer__divider"></i>
<button class="tiny-process-designer__btn" :title="t('ui.processDesigner.zoomIn')" @click="zoomIn">
<icon-zoom-in class="tiny-svg-size" />
</button>
<button class="tiny-process-designer__btn" :title="t('ui.processDesigner.zoomOut')" @click="zoomOut">
<icon-zoom-out class="tiny-svg-size" />
</button>
<button class="tiny-process-designer__btn" :title="t('ui.processDesigner.fitViewport')" @click="fitViewport">
<icon-fullscreen class="tiny-svg-size" />
</button>
<i class="tiny-process-designer__divider"></i>
<button class="tiny-process-designer__btn" :title="t('ui.processDesigner.downloadXml')" @click="downloadXML">
<icon-download class="tiny-svg-size" />
<span class="tiny-process-designer__btn-label">XML</span>
</button>
<button class="tiny-process-designer__btn" :title="t('ui.processDesigner.downloadSvg')" @click="downloadSVG">
<icon-download class="tiny-svg-size" />
<span class="tiny-process-designer__btn-label">SVG</span>
</button>
</slot>
</div>
<div ref="canvas" class="tiny-process-designer__canvas"></div>
</div>
</template>
<script lang="tsx">
import { renderless, api } from '@opentiny/vue-renderless/process-designer/vue'
import { props, setup, defineComponent } from '@opentiny/vue-common'
import { iconZoomIn, iconZoomOut, iconUndo, iconRedo, iconFullscreen, iconDownload } from '@opentiny/vue-icon'
import 'bpmn-js/dist/assets/diagram-js.css'
import 'bpmn-js/dist/assets/bpmn-font/css/bpmn.css'
import 'bpmn-js/dist/assets/bpmn-js.css'
import '@opentiny/vue-theme/process-designer/index.less'
export default defineComponent({
emits: [
'ready',
'import-done',
'import-error',
'save-xml',
'save-svg',
'element.click',
'element.dblclick',
'element.hover',
'element.out',
'element.changed',
'shape.added',
'shape.removed',
'connection.added',
'connection.removed',
'commandStack.changed',
'selection.changed',
'import.done',
'saveXML.done'
],
props: [...props, 'value', 'xml', 'readonly', 'showToolbar', 'height', 'fileName', 'options'],
components: {
IconZoomIn: iconZoomIn(),
IconZoomOut: iconZoomOut(),
IconUndo: iconUndo(),
IconRedo: iconRedo(),
IconFullscreen: iconFullscreen(),
IconDownload: iconDownload()
},
setup(props, context) {
return setup({ props, context, renderless, api })
}
})
</script>