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 @@ + + + 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 @@ + + + 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-jsViewer

\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.clickselection.changedcommandStack.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 @@ + + + +