diff --git a/examples/sites/demos/apis/process-designer.js b/examples/sites/demos/apis/process-designer.js
new file mode 100644
index 000000000..61b65c73d
--- /dev/null
+++ b/examples/sites/demos/apis/process-designer.js
@@ -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']
+ }
+ ]
+ }
+ ]
+}
diff --git a/examples/sites/demos/pc/app/process-designer/basic-usage-composition-api.vue b/examples/sites/demos/pc/app/process-designer/basic-usage-composition-api.vue
new file mode 100644
index 000000000..5d6be0e83
--- /dev/null
+++ b/examples/sites/demos/pc/app/process-designer/basic-usage-composition-api.vue
@@ -0,0 +1,44 @@
+
+
+
+
+
diff --git a/examples/sites/demos/pc/app/process-designer/basic-usage.spec.ts b/examples/sites/demos/pc/app/process-designer/basic-usage.spec.ts
new file mode 100644
index 000000000..e3a8a1b17
--- /dev/null
+++ b/examples/sites/demos/pc/app/process-designer/basic-usage.spec.ts
@@ -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)
+})
diff --git a/examples/sites/demos/pc/app/process-designer/basic-usage.vue b/examples/sites/demos/pc/app/process-designer/basic-usage.vue
new file mode 100644
index 000000000..b4b7a2582
--- /dev/null
+++ b/examples/sites/demos/pc/app/process-designer/basic-usage.vue
@@ -0,0 +1,55 @@
+
+
+
+
+
diff --git a/examples/sites/demos/pc/app/process-designer/events-composition-api.vue b/examples/sites/demos/pc/app/process-designer/events-composition-api.vue
new file mode 100644
index 000000000..e3ee9ea55
--- /dev/null
+++ b/examples/sites/demos/pc/app/process-designer/events-composition-api.vue
@@ -0,0 +1,21 @@
+
+
+
+
当前选中:{{ selectedId || '无' }}
+
+
+
+
diff --git a/examples/sites/demos/pc/app/process-designer/events.spec.ts b/examples/sites/demos/pc/app/process-designer/events.spec.ts
new file mode 100644
index 000000000..87c7e3686
--- /dev/null
+++ b/examples/sites/demos/pc/app/process-designer/events.spec.ts
@@ -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()
+})
diff --git a/examples/sites/demos/pc/app/process-designer/events.vue b/examples/sites/demos/pc/app/process-designer/events.vue
new file mode 100644
index 000000000..d2fe0357b
--- /dev/null
+++ b/examples/sites/demos/pc/app/process-designer/events.vue
@@ -0,0 +1,32 @@
+
+
+
+
当前选中:{{ selectedId || '无' }}
+
+
+
+
diff --git a/examples/sites/demos/pc/app/process-designer/readonly-composition-api.vue b/examples/sites/demos/pc/app/process-designer/readonly-composition-api.vue
new file mode 100644
index 000000000..41be51378
--- /dev/null
+++ b/examples/sites/demos/pc/app/process-designer/readonly-composition-api.vue
@@ -0,0 +1,31 @@
+
+
+
+
+
diff --git a/examples/sites/demos/pc/app/process-designer/readonly.spec.ts b/examples/sites/demos/pc/app/process-designer/readonly.spec.ts
new file mode 100644
index 000000000..37d4e2e66
--- /dev/null
+++ b/examples/sites/demos/pc/app/process-designer/readonly.spec.ts
@@ -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()
+})
diff --git a/examples/sites/demos/pc/app/process-designer/readonly.vue b/examples/sites/demos/pc/app/process-designer/readonly.vue
new file mode 100644
index 000000000..b8a2f6c78
--- /dev/null
+++ b/examples/sites/demos/pc/app/process-designer/readonly.vue
@@ -0,0 +1,37 @@
+
+
+
+
+
diff --git a/examples/sites/demos/pc/app/process-designer/webdoc/process-designer.cn.md b/examples/sites/demos/pc/app/process-designer/webdoc/process-designer.cn.md
new file mode 100644
index 000000000..d4241fd5f
--- /dev/null
+++ b/examples/sites/demos/pc/app/process-designer/webdoc/process-designer.cn.md
@@ -0,0 +1,7 @@
+---
+title: ProcessDesigner 流程设计器
+---
+
+# ProcessDesigner 流程设计器
+
+基于 [bpmn-js](https://github.com/bpmn-io/bpmn-js) 封装的 BPMN 2.0 流程图设计器组件,支持在画布上图形化绘制业务流程,提供撤销 / 重做 / 缩放 / 导出 XML / 导出 SVG 等基础能力,可用于工作流配置、审批流编排等场景。
diff --git a/examples/sites/demos/pc/app/process-designer/webdoc/process-designer.en.md b/examples/sites/demos/pc/app/process-designer/webdoc/process-designer.en.md
new file mode 100644
index 000000000..1ac86f65a
--- /dev/null
+++ b/examples/sites/demos/pc/app/process-designer/webdoc/process-designer.en.md
@@ -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.
diff --git a/examples/sites/demos/pc/app/process-designer/webdoc/process-designer.js b/examples/sites/demos/pc/app/process-designer/webdoc/process-designer.js
new file mode 100644
index 000000000..77ec5d84e
--- /dev/null
+++ b/examples/sites/demos/pc/app/process-designer/webdoc/process-designer.js
@@ -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':
+ '
通过 value 传入 BPMN 2.0 XML 即可渲染流程图,可在画布上拖拽编辑,使用工具栏完成撤销 / 重做 / 缩放 / 导出。
\n',
+ 'en-US':
+ 'Pass BPMN 2.0 XML via value to render the diagram. The diagram can be edited on the canvas, with the toolbar offering undo / redo / zoom / export.
\n'
+ },
+ codeFiles: ['basic-usage.vue']
+ },
+ {
+ demoId: 'readonly',
+ name: {
+ 'zh-CN': '只读模式',
+ 'en-US': 'Readonly'
+ },
+ desc: {
+ 'zh-CN':
+ '设置 readonly 属性后只渲染流程图、不可编辑,适用于流程预览场景。底层使用 bpmn-js 的 Viewer。
\n',
+ 'en-US':
+ 'With readonly set, the diagram is rendered without editing capabilities — suitable for read-only preview. Backed by bpmn-js Viewer.
\n'
+ },
+ codeFiles: ['readonly.vue']
+ },
+ {
+ demoId: 'events',
+ name: {
+ 'zh-CN': '事件监听',
+ 'en-US': 'Events'
+ },
+ desc: {
+ 'zh-CN':
+ '组件透传 bpmn-js 的常用事件,可监听 element.click、selection.changed、commandStack.changed 等。
\n',
+ 'en-US':
+ 'Common bpmn-js events are forwarded, such as element.click, selection.changed, and commandStack.changed.
\n'
+ },
+ codeFiles: ['events.vue']
+ }
+ ]
+}
diff --git a/examples/sites/demos/pc/menus.js b/examples/sites/demos/pc/menus.js
index c0cb74a3a..2e20b9e61 100644
--- a/examples/sites/demos/pc/menus.js
+++ b/examples/sites/demos/pc/menus.js
@@ -262,6 +262,7 @@ export const cmpMenus = [
{ 'nameCn': '图片', 'name': 'Image', 'key': 'image' },
{ 'nameCn': '无限滚动', 'name': 'InfiniteScroll', 'key': 'infinite-scroll' },
{ 'nameCn': '里程碑', 'name': 'Milestone', 'key': 'milestone' },
+ { 'nameCn': '流程设计器', 'name': 'ProcessDesigner', 'key': 'process-designer' },
{
'nameCn': '思维导图',
'name': 'MindMap',
diff --git a/packages/renderless/package.json b/packages/renderless/package.json
index 6413fb4d5..2ca7ab79b 100644
--- a/packages/renderless/package.json
+++ b/packages/renderless/package.json
@@ -40,6 +40,14 @@
"@opentiny/vue-hooks": "workspace:~",
"color": "4.2.3"
},
+ "peerDependencies": {
+ "bpmn-js": "^17.0.0"
+ },
+ "peerDependenciesMeta": {
+ "bpmn-js": {
+ "optional": true
+ }
+ },
"devDependencies": {
"esno": "^4.7.0",
"tsup": "7.2.0"
diff --git a/packages/renderless/src/process-designer/index.ts b/packages/renderless/src/process-designer/index.ts
new file mode 100644
index 000000000..e4c6e868f
--- /dev/null
+++ b/packages/renderless/src/process-designer/index.ts
@@ -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 = `
+
+
+
+
+
+
+
+
+
+
+
+`
+
+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
+ }
+ }
diff --git a/packages/renderless/src/process-designer/vue.ts b/packages/renderless/src/process-designer/vue.ts
new file mode 100644
index 000000000..5cc964226
--- /dev/null
+++ b/packages/renderless/src/process-designer/vue.ts
@@ -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
+}
diff --git a/packages/theme/src/index.less b/packages/theme/src/index.less
index c5eaf7c74..58bcc6c45 100644
--- a/packages/theme/src/index.less
+++ b/packages/theme/src/index.less
@@ -101,6 +101,7 @@
@import './popeditor/index.less';
@import './popover/index.less';
@import './popup/index.less';
+@import './process-designer/index.less';
@import './progress/index.less';
@import './qr-code/index.less';
@import './quarter-panel/index.less';
diff --git a/packages/theme/src/process-designer/index.less b/packages/theme/src/process-designer/index.less
new file mode 100644
index 000000000..81ded971d
--- /dev/null
+++ b/packages/theme/src/process-designer/index.less
@@ -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;
+ }
+}
diff --git a/packages/theme/src/process-designer/vars.less b/packages/theme/src/process-designer/vars.less
new file mode 100644
index 000000000..42c6be066
--- /dev/null
+++ b/packages/theme/src/process-designer/vars.less
@@ -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);
+}
diff --git a/packages/vue-locale/src/lang/en.ts b/packages/vue-locale/src/lang/en.ts
index f8e61458f..5f97bdd01 100644
--- a/packages/vue-locale/src/lang/en.ts
+++ b/packages/vue-locale/src/lang/en.ts
@@ -479,6 +479,15 @@ export default {
hide: 'Hide 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: {
moreText: 'More',
navigation: 'Navigation',
diff --git a/packages/vue-locale/src/lang/zh-CN.ts b/packages/vue-locale/src/lang/zh-CN.ts
index 935db585f..138fe780c 100644
--- a/packages/vue-locale/src/lang/zh-CN.ts
+++ b/packages/vue-locale/src/lang/zh-CN.ts
@@ -474,6 +474,15 @@ export default {
hide: '隐藏侧边栏',
show: '显示侧边栏'
},
+ processDesigner: {
+ undo: '撤销',
+ redo: '重做',
+ zoomIn: '放大',
+ zoomOut: '缩小',
+ fitViewport: '适配画布',
+ downloadXml: '下载 XML',
+ downloadSvg: '下载 SVG'
+ },
navMenu: {
moreText: '更多',
navigation: '导航',
diff --git a/packages/vue/index.ts b/packages/vue/index.ts
index d4b1e1d6d..418470396 100644
--- a/packages/vue/index.ts
+++ b/packages/vue/index.ts
@@ -120,6 +120,7 @@ import Popconfirm from '@opentiny/vue-popconfirm'
import Popeditor from '@opentiny/vue-popeditor'
import Popover from '@opentiny/vue-popover'
import Popup from '@opentiny/vue-popup'
+import ProcessDesigner from '@opentiny/vue-process-designer'
import Progress from '@opentiny/vue-progress'
import PullRefresh from '@opentiny/vue-pull-refresh'
import QrCode from '@opentiny/vue-qr-code'
@@ -322,6 +323,7 @@ const components = [
Popeditor,
Popover,
Popup,
+ ProcessDesigner,
Progress,
PullRefresh,
QrCode,
@@ -665,6 +667,8 @@ export {
Popover as TinyPopover,
Popup,
Popup as TinyPopup,
+ ProcessDesigner,
+ ProcessDesigner as TinyProcessDesigner,
Progress,
Progress as TinyProgress,
PullRefresh,
@@ -1066,6 +1070,8 @@ export default {
TinyPopover: Popover,
Popup,
TinyPopup: Popup,
+ ProcessDesigner,
+ TinyProcessDesigner: ProcessDesigner,
Progress,
TinyProgress: Progress,
PullRefresh,
diff --git a/packages/vue/package.json b/packages/vue/package.json
index 0aac7acc3..16ccfef50 100644
--- a/packages/vue/package.json
+++ b/packages/vue/package.json
@@ -154,6 +154,7 @@
"@opentiny/vue-popeditor": "workspace:~",
"@opentiny/vue-popover": "workspace:~",
"@opentiny/vue-popup": "workspace:~",
+ "@opentiny/vue-process-designer": "workspace:~",
"@opentiny/vue-progress": "workspace:~",
"@opentiny/vue-pull-refresh": "workspace:~",
"@opentiny/vue-qr-code": "workspace:~",
diff --git a/packages/vue/pc.ts b/packages/vue/pc.ts
index 4888a44dd..595c88a6f 100644
--- a/packages/vue/pc.ts
+++ b/packages/vue/pc.ts
@@ -106,6 +106,7 @@ import Popconfirm from '@opentiny/vue-popconfirm'
import Popeditor from '@opentiny/vue-popeditor'
import Popover from '@opentiny/vue-popover'
import Popup from '@opentiny/vue-popup'
+import ProcessDesigner from '@opentiny/vue-process-designer'
import Progress from '@opentiny/vue-progress'
import QrCode from '@opentiny/vue-qr-code'
import QuarterPanel from '@opentiny/vue-quarter-panel'
@@ -282,6 +283,7 @@ const components = [
Popeditor,
Popover,
Popup,
+ ProcessDesigner,
Progress,
QrCode,
QuarterPanel,
@@ -588,6 +590,8 @@ export {
Popover as TinyPopover,
Popup,
Popup as TinyPopup,
+ ProcessDesigner,
+ ProcessDesigner as TinyProcessDesigner,
Progress,
Progress as TinyProgress,
QrCode,
@@ -937,6 +941,8 @@ export default {
TinyPopover: Popover,
Popup,
TinyPopup: Popup,
+ ProcessDesigner,
+ TinyProcessDesigner: ProcessDesigner,
Progress,
TinyProgress: Progress,
QrCode,
diff --git a/packages/vue/src/process-designer/index.ts b/packages/vue/src/process-designer/index.ts
new file mode 100644
index 000000000..b86da626a
--- /dev/null
+++ b/packages/vue/src/process-designer/index.ts
@@ -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
diff --git a/packages/vue/src/process-designer/package.json b/packages/vue/src/process-designer/package.json
new file mode 100644
index 000000000..20dd62776
--- /dev/null
+++ b/packages/vue/src/process-designer/package.json
@@ -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:"
+ }
+}
diff --git a/packages/vue/src/process-designer/src/index.ts b/packages/vue/src/process-designer/src/index.ts
new file mode 100644
index 000000000..23fc1ff76
--- /dev/null
+++ b/packages/vue/src/process-designer/src/index.ts
@@ -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 })
+ }
+})
diff --git a/packages/vue/src/process-designer/src/pc.vue b/packages/vue/src/process-designer/src/pc.vue
new file mode 100644
index 000000000..186916468
--- /dev/null
+++ b/packages/vue/src/process-designer/src/pc.vue
@@ -0,0 +1,100 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+