@bpmn-nova/react 0.3.4-preview → 0.3.5-preview
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +59 -6
- package/dist/index.d.ts +68 -0
- package/dist/index.js +48 -3
- package/dist/styles.css +72 -12
- package/llms-full.txt +1545 -92
- package/llms.txt +62 -15
- package/package.json +2 -2
package/llms-full.txt
CHANGED
|
@@ -75,6 +75,8 @@ Use `engine="flowable"` or `engine="activiti"` explicitly from the host workflow
|
|
|
75
75
|
|
|
76
76
|
Studio modes are presentation state: `design` means editable process design, `viewer` means read-only process viewing, and `instance` means approval trace. Observe the actual Shell state through `subscribeMode()` or framework mode events; never store it in the BPMN model.
|
|
77
77
|
|
|
78
|
+
Node geometry is also presentation, not DI normalization. Loading server XML via the component parses its BPMN DI Bounds; XML export already writes model dimensions. The `0.3.5-preview` gateway/data fix scales standard shells to fit those bounds and docks display endpoints without rewriting model geometry or waypoints. Do not resize stored nodes, recreate them, or patch internal CSS to compensate. See [Node DI and visual geometry](docs/API.md#节点-di-与视觉尺寸) and the customization section in the co-located `llms-full.txt`; `node-geometry` is internal, not another installable package.
|
|
79
|
+
|
|
78
80
|
## 4. Mount a minimal integration
|
|
79
81
|
|
|
80
82
|
Every branch must import its own public `styles.css` exactly once and provide a height through the complete parent layout chain.
|
|
@@ -90,6 +92,10 @@ import '@bpmn-nova/vue/styles.css'
|
|
|
90
92
|
const props = defineProps({ initialXml: String })
|
|
91
93
|
const emit = defineEmits(['change'])
|
|
92
94
|
const studioRef = ref(null)
|
|
95
|
+
const config = {
|
|
96
|
+
modeling: { allowedEdgeTypes: ['sequenceFlow'] },
|
|
97
|
+
ui: { controlSize: 'medium' },
|
|
98
|
+
}
|
|
93
99
|
|
|
94
100
|
function handleChange(model, reason, xml) {
|
|
95
101
|
emit('change', xml)
|
|
@@ -102,8 +108,8 @@ function handleChange(model, reason, xml) {
|
|
|
102
108
|
ref="studioRef"
|
|
103
109
|
:xml="props.initialXml"
|
|
104
110
|
engine="flowable"
|
|
111
|
+
:config="config"
|
|
105
112
|
mode="design"
|
|
106
|
-
:allowed-edge-types="['sequenceFlow']"
|
|
107
113
|
:allowed-modes="['design', 'viewer']"
|
|
108
114
|
theme="auto"
|
|
109
115
|
@change="handleChange"
|
|
@@ -119,6 +125,11 @@ import { useRef } from 'react'
|
|
|
119
125
|
import { BpmnStudio } from '@bpmn-nova/react'
|
|
120
126
|
import '@bpmn-nova/react/styles.css'
|
|
121
127
|
|
|
128
|
+
const STUDIO_CONFIG = {
|
|
129
|
+
modeling: { allowedEdgeTypes: ['sequenceFlow'] },
|
|
130
|
+
ui: { controlSize: 'medium' },
|
|
131
|
+
}
|
|
132
|
+
|
|
122
133
|
export function WorkflowEditor({ initialXml, saveDraft }) {
|
|
123
134
|
const studioRef = useRef(null)
|
|
124
135
|
return (
|
|
@@ -127,8 +138,8 @@ export function WorkflowEditor({ initialXml, saveDraft }) {
|
|
|
127
138
|
ref={studioRef}
|
|
128
139
|
xml={initialXml}
|
|
129
140
|
engine="flowable"
|
|
141
|
+
config={STUDIO_CONFIG}
|
|
130
142
|
mode="design"
|
|
131
|
-
allowedEdgeTypes={['sequenceFlow']}
|
|
132
143
|
allowedModes={['design', 'viewer']}
|
|
133
144
|
theme="auto"
|
|
134
145
|
onChange={(model, reason, xml) => saveDraft(xml)}
|
|
@@ -148,13 +159,19 @@ import {
|
|
|
148
159
|
} from '@bpmn-nova/studio'
|
|
149
160
|
import '@bpmn-nova/studio/styles.css'
|
|
150
161
|
|
|
162
|
+
const config = {
|
|
163
|
+
modeling: { allowedEdgeTypes: ['sequenceFlow'] },
|
|
164
|
+
ui: { controlSize: 'medium' },
|
|
165
|
+
}
|
|
166
|
+
|
|
151
167
|
const studio = createStudioController({
|
|
152
168
|
model: createEmptyProcess('flowable'),
|
|
153
|
-
|
|
169
|
+
config,
|
|
154
170
|
})
|
|
155
171
|
const shell = createStudioShell({
|
|
156
172
|
container: document.querySelector('#workflow-studio'),
|
|
157
173
|
studio,
|
|
174
|
+
config,
|
|
158
175
|
mode: 'design',
|
|
159
176
|
allowedModes: ['design', 'viewer'],
|
|
160
177
|
theme: 'auto',
|
|
@@ -177,18 +194,16 @@ This step is complete when the workbench is visible, the initial XML is rendered
|
|
|
177
194
|
|
|
178
195
|
### 4.1 Embed the complete Studio in an existing business workbench
|
|
179
196
|
|
|
180
|
-
`BpmnStudio` is a complete Header + Palette + Canvas + Properties + Statusbar workbench by default. Preserve that default unless the host already owns a region. To keep the Nova Palette and editing tools, replace only the Header start area and explicitly hide Nova Properties:
|
|
197
|
+
`BpmnStudio` is a complete Header + Palette + Canvas + Properties + Statusbar workbench by default. Preserve that default unless the host already owns a region. Define a stable `studioConfig` object in host code. To keep the Nova Palette and editing tools, replace only the Header start area and explicitly hide Nova Properties:
|
|
181
198
|
|
|
182
199
|
```vue
|
|
183
200
|
<BpmnStudio
|
|
184
201
|
ref="studioRef"
|
|
185
202
|
:xml="xml"
|
|
186
203
|
engine="activiti"
|
|
204
|
+
:config="studioConfig"
|
|
187
205
|
mode="design"
|
|
188
206
|
:allowed-modes="['design']"
|
|
189
|
-
:allowed-node-types="supportedNodeTypes"
|
|
190
|
-
:allowed-edge-types="['sequenceFlow']"
|
|
191
|
-
:regions="{ right: 'hidden' }"
|
|
192
207
|
theme="auto"
|
|
193
208
|
@change="handleChange"
|
|
194
209
|
@selection-change="handleSelectionChange"
|
|
@@ -196,23 +211,43 @@ This step is complete when the workbench is visible, the initial XML is rendered
|
|
|
196
211
|
<template #header-start="{ state, actions }">
|
|
197
212
|
<!-- Host back action, business icon, process name, and type -->
|
|
198
213
|
</template>
|
|
199
|
-
<template #header-actions="{ actions, mode }">
|
|
214
|
+
<template #header-actions="{ actions, mode, ui }">
|
|
200
215
|
<!-- Host Validate / Save / Publish actions. Validation calls actions.validate(). -->
|
|
201
216
|
</template>
|
|
202
217
|
</BpmnStudio>
|
|
203
218
|
```
|
|
204
219
|
|
|
205
|
-
- Vue uses native `#header-start` / `#header-actions` / `#header` slots. React uses `headerStart` / `headerActions` / `header` render props. Core uses matching DOM Slots.
|
|
220
|
+
- Vue uses native `#header-start` / `#header-actions` / `#header` / `#right` slots. React uses `headerStart` / `headerActions` / `header` / `right` render props. Core uses matching DOM Slots. Native Right support starts in `0.3.5-preview`; inspect the installed declarations before using it.
|
|
206
221
|
- Vue supports `v-model:mode` and `mode-change`; React supports `mode` and `onModeChange`. Framework Header contexts expose the actual reactive `mode`. Core Slots use `getMode()` / `subscribeMode()`.
|
|
207
222
|
- Header Start replaces only the Nova Brand. Header Actions replaces only the default Validate / Import / Export group, so a host can compose Validate / Save / Publish without rebuilding the editing tools. A complete Header replacement calls the public Actions Interface.
|
|
208
223
|
- `actions.validate()` updates Nova's default status summary before emitting a read-only validation result with `source: 'toolbar'`; `shell.validate()` and framework Handle/Expose methods use `source: 'api'`. Vue receives `validation`, React receives `onValidation`, and Core uses `subscribeValidation()`. `valid` means there are no errors; warnings do not block publishing by default.
|
|
209
224
|
- The default Header does not duplicate Fit; use the footer control or `fitView()`.
|
|
210
|
-
- `regions` can hide `header`, `left`, `right`, or `footer` without leaving an empty layout track.
|
|
211
|
-
-
|
|
225
|
+
- `config.ui.regions` can hide `header`, `left`, `right`, or `footer` without leaving an empty layout track. Config changes update the existing Shell; do not rebuild the Canvas.
|
|
226
|
+
- A host panel can replace right content inside Nova, or live beside the Nova root after explicitly hiding the whole region. For an external design panel use `selection-change` / `onSelectionChange`, covering nodes, edges, multi-selection, clearing, and keyboard selection. Do not substitute `element-click` / `onElementClick`. Use the new `panelSelection` context for selection across read-only modes.
|
|
212
227
|
- Join host business configuration by stable BPMN element ID. Saving, publishing, authorization, upload, and server transactions remain host responsibilities.
|
|
213
228
|
- `theme="auto"` explicitly follows the system theme. The compatibility default remains `light`.
|
|
214
229
|
- Vue and React both export standalone `BpmnPalettePanel` for a fully custom layout. Pass the same external Studio Controller to Canvas, Palette, and Properties.
|
|
215
230
|
|
|
231
|
+
Group static Studio options under `config`: modeling constraints in `config.modeling`, Header layout in `config.ui`, Viewer display defaults in `config.viewer`, and SVG defaults in `config.export`. Keep model/XML, mode/allowedModes/runtime/projection/theme, resolvers, renderers, registries, callbacks, and Slots at the top level. Deprecated top-level configuration aliases remain compatible and explicitly win when the same item is supplied in both places.
|
|
232
|
+
|
|
233
|
+
`config.ui.controlSize` defaults to `medium` (32px). `small`, `medium`, and `large` map to 28px, 32px, and 40px; Element Plus `default` and Ant Design `middle` both map to Nova `medium`. A host-specific value must be a strict `24px`–`48px` string such as `36px`; numbers, other units, CSS expressions, and out-of-range values are invalid. Header contexts expose read-only `ui.controlSize` and `ui.controlHeight`, while custom actions inherit `--nova-control-height`, `--nova-header-height`, `--nova-control-font-size`, `--nova-control-padding-inline`, `--nova-control-icon-size`, and `--nova-control-radius`. These sizes never change footer zoom, canvas floating controls, Palette, or Properties.
|
|
234
|
+
|
|
235
|
+
Core Shell and framework handles expose `getConfig()` and `setConfig()`. The setter validates first and updates UI, Viewer, and Export in place without changing Model, XML, History, Selection, Mode, or Viewport. Modeling allowlists are Controller creation invariants; use the existing `setPropertiesProfile()` only for a runtime Profile change.
|
|
236
|
+
|
|
237
|
+
### 4.2 Sidebar layout and collapse (0.3.5-preview)
|
|
238
|
+
|
|
239
|
+
Read `SOURCE: docs/CUSTOMIZATION.md`, section `侧栏布局与平滑折叠`, in the co-located `llms-full.txt` before integrating the sidebar interfaces introduced in `0.3.5-preview`. They are not available in the older `0.3.4-preview` package. The host must still provide a computable container height.
|
|
240
|
+
|
|
241
|
+
- Configure `config.ui.leftPanel/rightPanel.collapsible` (default `true`) and `defaultCollapsed` (wide-layout initialization only, default `false`). `config.ui.rightPanel.layout` defaults to `flex`: Nova supplies a bounded column, the host arranges fixed regions and a `flex: 1; min-height: 0; overflow: auto` body. Use `scroll` for Nova-owned scrolling of the entire content; avoid nested scroll containers unless intentional.
|
|
242
|
+
- Core `slots.right`, Vue `#right`, and React `right` replace only content. Native slots preserve the host application tree and win over same-name Core DOM slots. Do not create another framework root or patch Nova classes. Header/Right contexts share `state`, `mode`, `ui`, and `panelSelection`, plus `studio`, `shell`, and `actions`.
|
|
243
|
+
- Design shows both panels by default; Viewer only the right; Instance hides both. An omitted right region uses mode defaults; explicit `regions.right: 'default'` opts into Instance, while `'hidden'` removes the panel and its toggle. A slot never overrides hidden. Preserve this distinction when reading/updating Config.
|
|
244
|
+
- Collapsed retains an edge toggle; hidden does not. Sidebars animate independently for 200ms and respect reduced motion. The first entry into a Shell container at or below 720px starts collapsible panels collapsed, with separate wide/narrow state memory. Expansion pushes the canvas, never opens a drawer. Content/form/scroll state survives; no automatic Fit occurs. Automatic Viewer projection responds once to the final size.
|
|
245
|
+
- Use `getSidebarState()`, `setSidebarCollapsed(side, collapsed)`, and `subscribeSidebarChange()`. The setter returns `true` only for actual changes; unavailable/redundant requests return `false`. Vue emits `sidebar-change`; React uses `onSidebarChange`. The read-only event contains `side`, `collapsed`, `previousCollapsed`, `mode`, and `source: button | api | responsive | config`.
|
|
246
|
+
- `panelSelection` contains `selection`, `selectedElement`, and `trace`. Core reads/subscribes with `getPanelSelection()` / `subscribePanelSelection()`. Design follows Controller selection; read-only clicks do not rewrite design selection. Do not substitute `state.selection` for the current approval-trace context. Hiding the sidebar does not disable trace clicks, details, or assets.
|
|
247
|
+
- These policies belong to the default Shell, not a complete custom `layout()` or standalone Designer/Viewer. Sidebar UI state must never enter BPMN XML, Model, or History.
|
|
248
|
+
|
|
249
|
+
### 4.3 Definition-time subtitle presentation
|
|
250
|
+
|
|
216
251
|
For a host-owned definition-time subtitle, pass `nodeSubtitleResolver` to Studio, Canvas, or Viewer. Return `undefined` for Nova's default subtitle, `null` to remove the row, or a string (including an empty string) as the override. After data inside a stable closure or Map changes, call `refreshPresentation()` on the component handle or Shell. This refresh must not be implemented by mutating a node, re-importing XML, or remounting the component. The resolver applies only to standard Design/Viewer task and container cards and standard SVG fallback; a complete custom renderer wins, and Instance runtime summaries are never overridden.
|
|
217
252
|
|
|
218
253
|
## 5. Keep model ownership deterministic
|
|
@@ -227,12 +262,16 @@ This step is complete when edit, undo, redo, save, reload, and intentional exter
|
|
|
227
262
|
|
|
228
263
|
## 6. Add host constraints before business use
|
|
229
264
|
|
|
230
|
-
Production workflow applications usually support a subset of BPMN. Configure `allowedNodeTypes` and `allowedEdgeTypes` on the Studio Controller or framework `BpmnStudio`; these reject unsupported imported models and block node/edge creation outside the host contract. The node allowlist filters Palette entries, but one allowed node type may still have multiple Palette presets. Configure matching Palette and Properties registries when the host needs custom labels or business fields.
|
|
265
|
+
Production workflow applications usually support a subset of BPMN. Configure `config.modeling.allowedNodeTypes` and `config.modeling.allowedEdgeTypes` on the Studio Controller or framework `BpmnStudio`; these reject unsupported imported models and block node/edge creation outside the host contract. The node allowlist filters Palette entries, but one allowed node type may still have multiple Palette presets. Configure matching Palette and Properties registries when the host needs custom labels or business fields.
|
|
231
266
|
|
|
232
267
|
Use `allowedModes` to expose only host-authorized workbench modes. Runtime changes use `setAllowedModes()` rather than rebuilding the Studio; invalid, empty, or duplicate lists are errors. Pass real Runtime data for instance mode; absence of Runtime data means no approval trace rather than demo business data.
|
|
233
268
|
|
|
234
269
|
Runtime snapshots store asset references only. The host supplies `runtimeAssetResolver`. BPMN Nova does not execute workflow engines, submit approvals, upload/store attachments, issue permissions, or export PNG/PDF.
|
|
235
270
|
|
|
271
|
+
For approval-trace integration, read the complete `SOURCE: docs/RUNTIME-INTEGRATION.md` section in the co-located `llms-full.txt` before mapping backend records. It covers instance-bound XML, stable work-item/visit identities, Flowable/Activiti mapping limits, complete approval scenarios, request cancellation, and assets. Runtime data stays top-level; only Studio display configuration belongs in `config.viewer`. Finish this branch only after the guide's relevant integration checks pass; report missing history instead of inventing facts.
|
|
272
|
+
|
|
273
|
+
Named Runtime type re-exports from the Vue/React facade start in `0.3.5-preview`. Check the installed declarations before using them; the guide gives a Props-based type fallback for older releases. Keep the one-direct-package rule rather than adding Studio for these types.
|
|
274
|
+
|
|
236
275
|
Read `llms-full.txt` before implementing custom Shell regions, Header composition, Actions, Palette, Properties, Context Menu, Renderer, Runtime, theme, or SVG export behavior. Use only public exports from the one selected package or supported Studio subpaths. `llms-full.txt` is generated by the Nova repository and must not be edited by hand.
|
|
237
276
|
|
|
238
277
|
## 7. Verify the integration
|
|
@@ -246,16 +285,24 @@ Run the target application's existing non-destructive quality commands, includin
|
|
|
246
285
|
5. Intentional external XML replacement loads once and resets history once.
|
|
247
286
|
6. Disallowed node types are unavailable and rejected by import/creation commands; disallowed modes are not rendered and `setMode()` returns `false` without changing state.
|
|
248
287
|
7. Theme changes update the existing instance.
|
|
249
|
-
8.
|
|
250
|
-
9.
|
|
251
|
-
10.
|
|
288
|
+
8. Default `medium`, presets, and a custom value such as `36px` resize only Header controls; invalid `config.ui.controlSize` leaves the previous DOM and configuration intact.
|
|
289
|
+
9. Unmount/remount leaves no duplicate listeners, overlays, or framework instances.
|
|
290
|
+
10. Browser Console has no errors.
|
|
291
|
+
11. If a subtitle resolver is used, verify override, `undefined`, `null`, SVG parity, and that `refreshPresentation()` preserves XML, history, selection, scope, zoom, pan, theme, and mode. Verify Instance summaries are unchanged.
|
|
292
|
+
12. If sidebars are used, verify independent collapse, narrow containers, reduced motion, keyboard focus, host form/scroll preservation, Flex/Scroll behavior, native slot updates, Instance defaults, and selection/event consistency. Keep XML, history, and viewport invariant during ordinary collapse; record automatic projection changes separately.
|
|
252
293
|
|
|
253
294
|
The installation is complete only when the package is present, the production build succeeds, and the relevant browser checks pass. Report any unverified branch explicitly.
|
|
254
295
|
|
|
296
|
+
## Maintainer release policy
|
|
297
|
+
|
|
298
|
+
Before changing Nova package versions, publishing, or recovering a partial release, read the complete `SOURCE: docs/NPM-PACKAGES.md` section in the co-located `llms-full.txt` (or that file in the source repository). It defines mandatory lockstep versions and full releases of all three public packages, including unchanged packages, plus Registry completion checks. This is a maintainer rule, not an instruction for host applications to install all three packages or authorization to publish without a user request.
|
|
299
|
+
|
|
255
300
|
## Reference
|
|
256
301
|
|
|
257
302
|
- `README.md`: human-facing product overview and quick start.
|
|
258
303
|
- `llms-full.txt`: complete generated AI context containing setup, component, Interface, customization, publishing, and architecture documentation.
|
|
304
|
+
- `docs/RUNTIME-INTEGRATION.md`: authoritative approval-trace guide in the source repository; the full content also ships inside every public package's `llms-full.txt`.
|
|
305
|
+
- `docs/NPM-PACKAGES.md`: authoritative global version and full-release policy, including partial-release recovery.
|
|
259
306
|
|
|
260
307
|
|
|
261
308
|
<!-- SOURCE: README.md -->
|
|
@@ -269,7 +316,7 @@ The installation is complete only when the package is present, the production bu
|
|
|
269
316
|
BPMN Nova 提供可嵌入的流程设计器、只读 Viewer、运行态审批轨迹、实例级主题、纯 SVG 导出,以及 React / Vue 适配。项目使用独立的 DOM / SVG 渲染实现,不依赖 `bpmn-js`,并为 Flowable 与 Activiti 提供 XML Profile 和属性扩展。
|
|
270
317
|
|
|
271
318
|
> [!IMPORTANT]
|
|
272
|
-
> 当前版本为 `0.3.
|
|
319
|
+
> 当前版本为 `0.3.5-preview`。它适合 SDK 评估、产品集成验证和企业审批原型;公开 Interface、BPMN XML round-trip 与引擎兼容能力仍在持续稳定中,建议使用 `@preview` 安装并在生产接入前完成目标流程验证。
|
|
273
320
|
|
|
274
321
|
AI 或代码生成工具接入必须从 [`llms.txt`](llms.txt) 的完整安装流程开始;需要全部接口与定制上下文时再读取 [`llms-full.txt`](llms-full.txt)。这两份文件也会随三个公开 npm 包发布。
|
|
275
322
|
|
|
@@ -397,16 +444,24 @@ const model = createEmptyProcess('flowable')
|
|
|
397
444
|
model.id = 'Process_PurchaseApproval'
|
|
398
445
|
model.name = '采购申请审批流程'
|
|
399
446
|
|
|
447
|
+
const config = {
|
|
448
|
+
modeling: {
|
|
449
|
+
propertiesProfile: 'business',
|
|
450
|
+
allowedNodeTypes: ['startEvent', 'userTask', 'exclusiveGateway', 'endEvent'],
|
|
451
|
+
allowedEdgeTypes: ['sequenceFlow'],
|
|
452
|
+
},
|
|
453
|
+
ui: { controlSize: 'medium' },
|
|
454
|
+
}
|
|
455
|
+
|
|
400
456
|
const studio = createStudioController({
|
|
401
457
|
model,
|
|
402
|
-
|
|
403
|
-
allowedNodeTypes: ['startEvent', 'userTask', 'exclusiveGateway', 'endEvent'],
|
|
404
|
-
allowedEdgeTypes: ['sequenceFlow'],
|
|
458
|
+
config,
|
|
405
459
|
})
|
|
406
460
|
|
|
407
461
|
const shell = createStudioShell({
|
|
408
462
|
container: document.querySelector('#bpmn-studio'),
|
|
409
463
|
studio,
|
|
464
|
+
config,
|
|
410
465
|
mode: 'design',
|
|
411
466
|
allowedModes: ['design'],
|
|
412
467
|
theme: 'auto',
|
|
@@ -430,6 +485,8 @@ const studio = createStudioController({ model })
|
|
|
430
485
|
|
|
431
486
|
### Viewer 与 Runtime
|
|
432
487
|
|
|
488
|
+
真实后端接入请从[审批轨迹接入指南](docs/RUNTIME-INTEGRATION.md)开始:包含匹配的 XML、Flowable/Activiti 映射、完整 Snapshot、会签与驳回重入、刷新和附件。Vue/React 的具名 Runtime 类型出口自 `0.3.5-preview` 提供;指南也提供旧版的兼容类型写法。
|
|
489
|
+
|
|
433
490
|
```js
|
|
434
491
|
import { BpmnViewer } from '@bpmn-nova/studio/viewer'
|
|
435
492
|
import '@bpmn-nova/studio/styles.css'
|
|
@@ -457,15 +514,22 @@ npm install @bpmn-nova/react@preview
|
|
|
457
514
|
import { BpmnStudio } from '@bpmn-nova/react'
|
|
458
515
|
import '@bpmn-nova/react/styles.css'
|
|
459
516
|
|
|
517
|
+
const studioConfig = {
|
|
518
|
+
modeling: {
|
|
519
|
+
propertiesProfile: 'business',
|
|
520
|
+
allowedNodeTypes: ['startEvent', 'userTask', 'exclusiveGateway', 'endEvent'],
|
|
521
|
+
allowedEdgeTypes: ['sequenceFlow'],
|
|
522
|
+
},
|
|
523
|
+
ui: { controlSize: 'medium' },
|
|
524
|
+
}
|
|
525
|
+
|
|
460
526
|
export function WorkflowEditor({ initialXml }) {
|
|
461
527
|
return (
|
|
462
528
|
<div style={{ height: 720 }}>
|
|
463
529
|
<BpmnStudio
|
|
464
530
|
xml={initialXml}
|
|
465
531
|
engine="flowable"
|
|
466
|
-
|
|
467
|
-
allowedNodeTypes={['startEvent', 'userTask', 'exclusiveGateway', 'endEvent']}
|
|
468
|
-
allowedEdgeTypes={['sequenceFlow']}
|
|
532
|
+
config={studioConfig}
|
|
469
533
|
mode="design"
|
|
470
534
|
allowedModes={['design']}
|
|
471
535
|
theme="auto"
|
|
@@ -492,6 +556,14 @@ import { BpmnStudio } from '@bpmn-nova/vue'
|
|
|
492
556
|
import '@bpmn-nova/vue/styles.css'
|
|
493
557
|
|
|
494
558
|
const props = defineProps({ initialXml: String })
|
|
559
|
+
const studioConfig = {
|
|
560
|
+
modeling: {
|
|
561
|
+
propertiesProfile: 'business',
|
|
562
|
+
allowedNodeTypes: ['startEvent', 'userTask', 'exclusiveGateway', 'endEvent'],
|
|
563
|
+
allowedEdgeTypes: ['sequenceFlow'],
|
|
564
|
+
},
|
|
565
|
+
ui: { controlSize: 'medium' },
|
|
566
|
+
}
|
|
495
567
|
|
|
496
568
|
function onChange(model, reason, nextXml) {
|
|
497
569
|
console.log(reason, nextXml)
|
|
@@ -503,9 +575,7 @@ function onChange(model, reason, nextXml) {
|
|
|
503
575
|
<BpmnStudio
|
|
504
576
|
:xml="props.initialXml"
|
|
505
577
|
engine="flowable"
|
|
506
|
-
|
|
507
|
-
:allowed-node-types="['startEvent', 'userTask', 'exclusiveGateway', 'endEvent']"
|
|
508
|
-
:allowed-edge-types="['sequenceFlow']"
|
|
578
|
+
:config="studioConfig"
|
|
509
579
|
mode="design"
|
|
510
580
|
:allowed-modes="['design']"
|
|
511
581
|
theme="auto"
|
|
@@ -528,9 +598,10 @@ Vue Expose 提供与 Vanilla 实例对应的 Actions、校验、XML、视图、
|
|
|
528
598
|
engine="activiti"
|
|
529
599
|
mode="design"
|
|
530
600
|
:allowed-modes="['design']"
|
|
531
|
-
:
|
|
532
|
-
|
|
533
|
-
|
|
601
|
+
:config="{
|
|
602
|
+
modeling: { allowedNodeTypes: supportedNodeTypes, allowedEdgeTypes: ['sequenceFlow'] },
|
|
603
|
+
ui: { controlSize: 'medium', regions: { right: 'hidden' } },
|
|
604
|
+
}"
|
|
534
605
|
theme="auto"
|
|
535
606
|
@change="handleChange"
|
|
536
607
|
@selection-change="handleSelectionChange"
|
|
@@ -539,7 +610,7 @@ Vue Expose 提供与 Vanilla 实例对应的 Actions、校验、XML、视图、
|
|
|
539
610
|
<template #header-start="{ state, actions }">
|
|
540
611
|
<!-- 返回、业务图标、流程名称和类型 -->
|
|
541
612
|
</template>
|
|
542
|
-
<template #header-actions="{ actions, mode }">
|
|
613
|
+
<template #header-actions="{ actions, mode, ui }">
|
|
543
614
|
<button type="button" :disabled="mode !== 'design'" @click="actions.validate()">校验</button>
|
|
544
615
|
<button type="button" :disabled="mode !== 'design'" @click="saveDraft(actions.exportXml())">保存</button>
|
|
545
616
|
<button type="button" :disabled="mode !== 'design'" @click="publishProcess(actions)">发布</button>
|
|
@@ -547,10 +618,26 @@ Vue Expose 提供与 Vanilla 实例对应的 Actions、校验、XML、视图、
|
|
|
547
618
|
</BpmnStudio>
|
|
548
619
|
```
|
|
549
620
|
|
|
550
|
-
`header-start` 只替换 Nova Brand;`header-actions` 只替换默认的“校验 / 导入 / 导出”动作组,适合宿主组合“校验 / 保存 / 发布”;`header` 可替换整个 Header。`regions` 隐藏区域后不会保留 Grid 空白,也不会重建 Canvas。React 提供等价的 `headerStart` / `headerActions` / `header` Render Prop,Core 提供同名 DOM Slots。默认 Header 的最佳视图只保留在底部缩放区。
|
|
621
|
+
`header-start` 只替换 Nova Brand;`header-actions` 只替换默认的“校验 / 导入 / 导出”动作组,适合宿主组合“校验 / 保存 / 发布”;`header` 可替换整个 Header。`config.ui.regions` 隐藏区域后不会保留 Grid 空白,也不会重建 Canvas。React 提供等价的 `headerStart` / `headerActions` / `header` Render Prop,Core 提供同名 DOM Slots。默认 Header 的最佳视图只保留在底部缩放区。
|
|
551
622
|
|
|
552
623
|
`actions.validate()` 会先更新 Nova 默认状态栏,再发送 `validation` 事件并返回 issues;公开 `validate()` 使用相同流程。`valid` 只在存在 error 时为 `false`,warning 会展示和上报但不默认阻止发布。
|
|
553
624
|
|
|
625
|
+
静态建模、Shell UI、Viewer 展示和 SVG 导出选项统一收敛在 `config`。顶部控件由 `config.ui.controlSize` 控制,默认 `medium`,可选 `small`、`medium`、`large` 或 `24px`–`48px`;该值只改变 Header,不改变底部缩放区、画布浮动工具、Palette 或 Properties。Header Slot 的 `ui.controlHeight` 可直接用于对齐宿主控件:Element Plus 的 `default`、Ant Design 的 `middle` 对应 Nova `medium`。自定义像素值时也可读取 `--nova-control-height` 等公开 CSS Variables。
|
|
626
|
+
|
|
627
|
+
```js
|
|
628
|
+
shell.setConfig({
|
|
629
|
+
ui: {
|
|
630
|
+
controlSize: '36px',
|
|
631
|
+
regions: { right: 'hidden' },
|
|
632
|
+
sidebarWidth: { left: 232, right: 344 },
|
|
633
|
+
},
|
|
634
|
+
})
|
|
635
|
+
|
|
636
|
+
console.log(shell.getConfig(), shell.ui.controlHeight) // 36px
|
|
637
|
+
```
|
|
638
|
+
|
|
639
|
+
旧的 `propertiesProfile`、`allowedNodeTypes`、`regions`、`responsive`、`timeline`、`svgExport` 等顶层字段继续兼容,但已标记为 Deprecated;同一项同时出现时,显式旧字段优先。`config.modeling` 只在 Controller 创建时建立约束,运行中节点和连线白名单不会被 `setConfig()` 改写。
|
|
640
|
+
|
|
554
641
|
发布事务由宿主实现,并可以直接复用同一个 Header Action 完成“校验后发布”:
|
|
555
642
|
|
|
556
643
|
```js
|
|
@@ -561,7 +648,24 @@ async function publishProcess(actions) {
|
|
|
561
648
|
}
|
|
562
649
|
```
|
|
563
650
|
|
|
564
|
-
|
|
651
|
+
宿主右侧属性面板可以通过右侧 Slot 复用 Nova 布局,也可以在显式隐藏 Nova 右侧后放在外部。外部设计面板以 `selection-change` / `onSelectionChange` 为状态来源,以稳定 BPMN Element ID 关联业务配置;`element-click` 不能替代选择事件。Nova 不拥有宿主的保存、发布、权限或服务端事务。
|
|
652
|
+
|
|
653
|
+
### 侧栏扩展、滚动与平滑折叠
|
|
654
|
+
|
|
655
|
+
Core `slots.right`、Vue `#right`、React `right` 可以替换右侧内容,Nova 继续管理宽度、高度和左右分隔线中点的折叠按钮。右侧默认 `flex`,适合固定标题/底部、中间内容滚动;设置 `scroll` 则由 Nova 滚动整个内容。
|
|
656
|
+
|
|
657
|
+
```js
|
|
658
|
+
const config = {
|
|
659
|
+
ui: {
|
|
660
|
+
leftPanel: { collapsible: true, defaultCollapsed: false },
|
|
661
|
+
rightPanel: { collapsible: true, defaultCollapsed: false, layout: 'flex' },
|
|
662
|
+
},
|
|
663
|
+
}
|
|
664
|
+
```
|
|
665
|
+
|
|
666
|
+
左右独立收起,200ms 平滑过渡并尊重减少动态效果;收起保留展开按钮,不卸载内容、不丢失表单/滚动状态、不自动 Fit。Shell 容器 ≤720px 时首次默认收起,展开继续挤压画布,宽窄屏分别记忆状态。**隐藏不同于收起**:`regions.right: 'hidden'` 移除整个区域及按钮;Instance 默认隐藏右侧,显式 `'default'` 可开启。
|
|
667
|
+
|
|
668
|
+
Shell 与框架 Handle/Expose 提供 `getSidebarState()`、`setSidebarCollapsed()`、`subscribeSidebarChange()`;Vue `sidebar-change`、React `onSidebarChange` 接收同一事件。Header/Right Context 的 `panelSelection` 支持设计与只读轨迹选择,Core 使用 `getPanelSelection()` / `subscribePanelSelection()`。完整 Core/Vue/React 用法、Flex CSS 和兼容说明见[侧栏指南](docs/CUSTOMIZATION.md#侧栏布局与平滑折叠)。本节新接口自 `0.3.5-preview` 提供。
|
|
565
669
|
|
|
566
670
|
`theme="auto"` 是显式启用系统主题适配;为了兼容已有接入,未传 Theme 时仍默认为 `light`。Vue 包同时提供独立的 `BpmnPalettePanel`,用于完全自定义布局。
|
|
567
671
|
|
|
@@ -760,6 +864,7 @@ shell.openSvgExportPreview()
|
|
|
760
864
|
|
|
761
865
|
## 浏览器接入注意事项
|
|
762
866
|
|
|
867
|
+
- 节点默认尺寸不等于导入尺寸:组件加载服务端 XML 也会解析 BPMN DI Bounds;保存/导出会写入模型宽高。自 `0.3.5-preview` 提供的网关与数据图元兼容修复只让内部图形按实际容器缩小居中,并校正显示连线,不扩大 DI、不改 waypoint。宿主无需配置节点尺寸、删除重建节点或覆盖内部 CSS,详见 [DI 与视觉尺寸](docs/API.md#节点-di-与视觉尺寸)。
|
|
763
868
|
- 为 Canvas、Studio 和 Viewer 容器设置明确高度;隐藏容器恢复显示后可调用 `fitView()`。
|
|
764
869
|
- CSS 必须由宿主显式导入;Studio、React、Vue 各自提供一个自包含样式入口。
|
|
765
870
|
- 使用 ESM 与现代浏览器;Node.js 18+ 用于构建和包消费工具链。
|
|
@@ -772,6 +877,7 @@ shell.openSvgExportPreview()
|
|
|
772
877
|
- [快速开始](docs/GETTING-STARTED.md):Studio、Designer、Viewer、React、Vue 与 XML。
|
|
773
878
|
- [组件参数](docs/COMPONENTS.md):Options、Props、事件、Ref / Expose 和实例方法。
|
|
774
879
|
- [API](docs/API.md):模型、命令、XML、Runtime Snapshot、主题与导出类型。
|
|
880
|
+
- [审批轨迹接入指南](docs/RUNTIME-INTEGRATION.md):宿主数据转换、组件接入、审批轮次、回退与资源解析。
|
|
775
881
|
- [自定义指南](docs/CUSTOMIZATION.md):主题、Palette、图标、Renderer、Slot 和运行态扩展。
|
|
776
882
|
- [Properties Panel](docs/PROPERTIES-PANEL.md):Provider、Group、Entry、Data Provider 和自定义字段。
|
|
777
883
|
- [NPM Packages](docs/NPM-PACKAGES.md):三个公开包、Studio 子路径、构建验证与 Registry 清理说明。
|
|
@@ -893,16 +999,24 @@ const model = createEmptyProcess('flowable')
|
|
|
893
999
|
model.id = 'Process_Approval'
|
|
894
1000
|
model.name = '审批流程'
|
|
895
1001
|
|
|
1002
|
+
const config = {
|
|
1003
|
+
modeling: {
|
|
1004
|
+
propertiesProfile: 'business',
|
|
1005
|
+
allowedNodeTypes: ['startEvent', 'userTask', 'exclusiveGateway', 'endEvent'],
|
|
1006
|
+
allowedEdgeTypes: ['sequenceFlow'],
|
|
1007
|
+
},
|
|
1008
|
+
ui: { controlSize: 'medium' },
|
|
1009
|
+
}
|
|
1010
|
+
|
|
896
1011
|
const studio = createStudioController({
|
|
897
1012
|
model,
|
|
898
|
-
|
|
899
|
-
allowedNodeTypes: ['startEvent', 'userTask', 'exclusiveGateway', 'endEvent'],
|
|
900
|
-
allowedEdgeTypes: ['sequenceFlow'],
|
|
1013
|
+
config,
|
|
901
1014
|
})
|
|
902
1015
|
|
|
903
1016
|
const shell = createStudioShell({
|
|
904
1017
|
container: document.querySelector('#studio'),
|
|
905
1018
|
studio,
|
|
1019
|
+
config,
|
|
906
1020
|
mode: 'design',
|
|
907
1021
|
allowedModes: ['design', 'viewer'],
|
|
908
1022
|
theme: 'auto',
|
|
@@ -974,6 +1088,11 @@ import { useRef, useState } from 'react'
|
|
|
974
1088
|
import { BpmnStudio } from '@bpmn-nova/react'
|
|
975
1089
|
import '@bpmn-nova/react/styles.css'
|
|
976
1090
|
|
|
1091
|
+
const STUDIO_CONFIG = {
|
|
1092
|
+
modeling: { propertiesProfile: 'business' },
|
|
1093
|
+
ui: { controlSize: 'medium' },
|
|
1094
|
+
}
|
|
1095
|
+
|
|
977
1096
|
export function WorkflowEditor({ initialXml }) {
|
|
978
1097
|
const studioRef = useRef(null)
|
|
979
1098
|
const [mode, setMode] = useState('design')
|
|
@@ -984,10 +1103,10 @@ export function WorkflowEditor({ initialXml }) {
|
|
|
984
1103
|
ref={studioRef}
|
|
985
1104
|
xml={initialXml}
|
|
986
1105
|
engine="flowable"
|
|
1106
|
+
config={STUDIO_CONFIG}
|
|
987
1107
|
mode={mode}
|
|
988
1108
|
allowedModes={['design', 'viewer']}
|
|
989
1109
|
theme="auto"
|
|
990
|
-
propertiesProfile="business"
|
|
991
1110
|
onChange={(model, reason, nextXml) => saveDraft(nextXml)}
|
|
992
1111
|
onModeChange={(event) => setMode(event.mode)}
|
|
993
1112
|
/>
|
|
@@ -1015,6 +1134,10 @@ import '@bpmn-nova/vue/styles.css'
|
|
|
1015
1134
|
const props = defineProps({ initialXml: String })
|
|
1016
1135
|
const studioRef = ref(null)
|
|
1017
1136
|
const mode = ref('design')
|
|
1137
|
+
const config = {
|
|
1138
|
+
modeling: { propertiesProfile: 'business' },
|
|
1139
|
+
ui: { controlSize: 'medium' },
|
|
1140
|
+
}
|
|
1018
1141
|
const onChange = (model, reason, nextXml) => saveDraft(nextXml)
|
|
1019
1142
|
</script>
|
|
1020
1143
|
|
|
@@ -1024,10 +1147,10 @@ const onChange = (model, reason, nextXml) => saveDraft(nextXml)
|
|
|
1024
1147
|
ref="studioRef"
|
|
1025
1148
|
:xml="props.initialXml"
|
|
1026
1149
|
engine="flowable"
|
|
1150
|
+
:config="config"
|
|
1027
1151
|
v-model:mode="mode"
|
|
1028
1152
|
:allowed-modes="['design', 'viewer']"
|
|
1029
1153
|
theme="auto"
|
|
1030
|
-
properties-profile="business"
|
|
1031
1154
|
@change="onChange"
|
|
1032
1155
|
@mode-change="event => console.log(event.source)"
|
|
1033
1156
|
/>
|
|
@@ -1039,28 +1162,48 @@ const onChange = (model, reason, nextXml) => saveDraft(nextXml)
|
|
|
1039
1162
|
|
|
1040
1163
|
### 4.1 宿主工作台组合规则
|
|
1041
1164
|
|
|
1042
|
-
默认 `BpmnStudio`
|
|
1165
|
+
默认 `BpmnStudio` 是完整工作台。静态建模、布局、Viewer 和导出选项统一放入 `config`;数据、状态、事件和 Slots 保持顶层。宿主内容可通过右侧 Slot 复用 Nova 布局;只有需要把整个面板放到 Nova 外部时,才使用 `config: { ui: { regions: { right: 'hidden' } } }`。隐藏区域不保留 Grid 轨道或展开按钮,不能用右侧 Slot 覆盖。
|
|
1043
1166
|
|
|
1044
|
-
Vue 使用原生 `#header-start` / `#header-actions` / `#header`,React 使用 `headerStart` / `headerActions` / `header` Render Prop。Header Start 只替换 Brand,Header Actions 只替换默认“校验 / 导入 / 导出”动作组,完整 Header 通过公开 `StudioShellActions`
|
|
1167
|
+
Vue 使用原生 `#header-start` / `#header-actions` / `#header` / `#right`,React 使用 `headerStart` / `headerActions` / `header` / `right` Render Prop。Header Start 只替换 Brand,Header Actions 只替换默认“校验 / 导入 / 导出”动作组,完整 Header 通过公开 `StudioShellActions` 重建所需编辑操作;Right 仅替换侧栏内容。不要创建第二个 Vue App 或 React Root 挂载这些内容。同名原生 Slot 优先于 Core DOM Slot。
|
|
1045
1168
|
|
|
1046
1169
|
宿主通常在 Header Actions 中组合“校验 / 保存 / 发布”。校验按钮调用 `actions.validate()`:Nova 先更新默认状态栏,再通过 Vue `validation`、React `onValidation` 或 Core `subscribeValidation()` 发送只读结果。程序化 `validate()` 的来源为 `api`,Header Action 的来源为 `toolbar`;`valid` 仅在存在 error 时为 `false`。保存、发布、权限和服务端事务始终留在宿主。顶部不再重复渲染最佳视图,底部缩放区及 `fitView()` Interface 继续可用。
|
|
1047
1170
|
|
|
1048
|
-
Header Slot/Render Context 的 `mode`
|
|
1171
|
+
Header Slot/Render Context 的 `mode` 是当前实际模式,`ui` 是只读的实际控件尺寸。宿主用它显隐保存、发布按钮、映射自己的组件库尺寸或外部属性面板;不要从 Nova Header DOM 读取选中按钮。运行时改变 `allowedModes` 会原地更新默认模式按钮,移除当前模式时只回退一次。
|
|
1172
|
+
|
|
1173
|
+
`config.ui.controlSize` 默认为 Nova `medium`(32px),对应 Element Plus `default` 和 Ant Design `middle`;`small`、`large` 分别对应两者的同名尺寸。宿主也可传 `24px`–`48px` 的严格 CSS 像素字符串,例如 `36px`。自定义 Header 控件可以继承 `--nova-control-height`、`--nova-header-height`、`--nova-control-font-size`、`--nova-control-padding-inline`、`--nova-control-icon-size` 和 `--nova-control-radius`。该尺寸边界只覆盖 Header,不影响底部缩放区、画布浮动工具、Palette 或 Properties。
|
|
1174
|
+
|
|
1175
|
+
`getConfig()` 返回实际归一化配置,`setConfig()` 在完整校验成功后原地更新 UI、Viewer 与 Export,并保持 Model、XML、History、Selection、Mode 和 Viewport。`config.modeling.allowedNodeTypes` 与 `allowedEdgeTypes` 只在 Controller 创建时建立不变量;运行时 Profile 仍通过 `setPropertiesProfile()` 修改。旧顶层配置仅作为 Deprecated 兼容别名;同一项新旧并存时,旧顶层显式值优先。
|
|
1049
1176
|
|
|
1050
1177
|
外部业务属性面板必须以 `selection-change` / `onSelectionChange` 为状态来源,以稳定 BPMN Element ID 关联业务配置。该事件覆盖节点、连线、多选、清空和键盘选择;`element-click` / `onElementClick` 只是点击观察事件。保存草稿、发布、权限和服务端事务属于宿主,不要写入 Nova Actions。
|
|
1051
1178
|
|
|
1052
1179
|
显式 `theme="auto"` 才表示跟随系统主题;默认 Theme 为兼容性的 `light`。Vue 与 React 都公开独立 `BpmnPalettePanel`,完整自定义布局时必须让 Canvas、Palette 和 Properties 复用同一个外部 Controller。
|
|
1053
1180
|
|
|
1054
|
-
### 4.
|
|
1181
|
+
### 4.2 侧栏布局与折叠门禁
|
|
1182
|
+
|
|
1183
|
+
新增侧栏接口自 `0.3.5-preview` 提供,先核对目标安装包的版本与声明。完整 Core/Vue/React 示例见[侧栏指南](CUSTOMIZATION.md#侧栏布局与平滑折叠),不要以内部 CSS 覆盖实现本能力。
|
|
1184
|
+
|
|
1185
|
+
- `config.ui.leftPanel/rightPanel` 收拢 `collapsible`(默认 `true`)、`defaultCollapsed`(宽屏初始化默认 `false`);右侧另有 `layout: 'flex' | 'scroll'`,默认 `flex`。不要添加零散顶层布局 Props。
|
|
1186
|
+
- Flex 由宿主安排固定头尾和弹性滚动区,Nova 仅提供有界纵向 Flex 容器;Scroll 由 Nova 滚动整个内容。宿主高度链必须可计算,不能只给无高度基准的根节点设置 `height: 100%`。
|
|
1187
|
+
- Design 默认显示左右,Viewer 仅右侧,Instance 默认两侧隐藏。右侧显式 `'default'` 可在 Instance 开启,`'hidden'` 移除内容和按钮;Slot 不改变显隐优先级。`getConfig()` 保留未配置意图,不应把读取结果中的默认值再转成显式开启。
|
|
1188
|
+
- Shell 容器 ≤720px 首次收起可折叠面板,保留按钮;宽窄屏分别记忆状态。两侧独立展开且挤压画布,非抽屉。200ms 动画尊重减少动态效果,不卸载 Slot、不自动 Fit;Viewer 自动投影仅在动画结束后按最终宽度处理。
|
|
1189
|
+
- 使用 `getSidebarState()` / `setSidebarCollapsed()` / `subscribeSidebarChange()`;Vue `sidebar-change`、React `onSidebarChange` 与 Core 事件一致。折叠是 Shell UI 状态,不写入 BPMN 模型或历史。
|
|
1190
|
+
- Header/Right Context 共用响应式 `mode`、`state`、`ui`、`panelSelection`。Core 使用 `getPanelSelection()` / `subscribePanelSelection()`;Viewer/Instance 的实际选择不回写 Controller 设计选择,也不能用设计 `state.selection` 代替。
|
|
1191
|
+
- 验证折叠按钮、键盘、减少动态效果、表单/滚动状态保留、跨模式选择更新和无重复订阅;完整自定义 `layout()` 与独立 Designer/Viewer 不接管默认侧栏。
|
|
1192
|
+
|
|
1193
|
+
### 4.3 模型所有权
|
|
1055
1194
|
|
|
1056
1195
|
框架组件的 `xml`/`model` 是外部替换输入,`change`/`onChange` 的 XML 是草稿输出。宿主可以保存该输出;如果响应式状态回写的是完全相同的导出 XML,Adapter 会忽略这次回声并保留 Undo/Redo 历史。只有加载另一流程或服务器 revision 时才传入不同 XML,此时模型和历史会替换一次。
|
|
1057
1196
|
|
|
1058
1197
|
React/Vue 包重新导出创建外部 Controller、Palette、Properties、Context Menu、Icon 和 Template Registry 所需的公开函数,因此高级接入仍只需要直接安装对应 Adapter 包。
|
|
1059
1198
|
|
|
1060
|
-
生产工作流通常只支持 BPMN 子集。通过 Controller 或 `BpmnStudio` 的 `allowedNodeTypes` 与 `allowedEdgeTypes` 声明节点、连线白名单;该限制会校验初始/外部模型,并约束创建、快捷新增、模板和节点类型转换。Palette 会服从节点白名单,但同一节点类型可以有多个 Preset
|
|
1199
|
+
生产工作流通常只支持 BPMN 子集。通过 Controller 或 `BpmnStudio` 的 `config.modeling.allowedNodeTypes` 与 `allowedEdgeTypes` 声明节点、连线白名单;该限制会校验初始/外部模型,并约束创建、快捷新增、模板和节点类型转换。Palette 会服从节点白名单,但同一节点类型可以有多个 Preset。通过顶层 `allowedModes` 限制工作台可切换模式。实例模式没有 Runtime 时保持空运行事实,不会自动加载演示数据。
|
|
1061
1200
|
|
|
1062
1201
|
## 5. Runtime Snapshot 与审批动作
|
|
1063
1202
|
|
|
1203
|
+
接入真实审批数据、会签、回退或附件前,先完整阅读[审批轨迹接入指南](RUNTIME-INTEGRATION.md)。使用 npm 包内上下文时读取 `llms-full.txt` 中 `SOURCE: docs/RUNTIME-INTEGRATION.md` 对应全文。按指南完成身份映射、快照转换、过期请求处理与场景验收;引擎字段缺失时向宿主报告缺口,不生成虚假的审批事实。
|
|
1204
|
+
|
|
1205
|
+
Vue/React 的 14 个具名 Runtime 类型出口自 `0.3.5-preview` 提供;旧安装版本使用指南中的 Props 推导,不增加 Studio 直依赖。`runtime` 保持顶层,Studio 展示配置进入 `config.viewer`,独立 Viewer 继续使用顶层展示 Options。
|
|
1206
|
+
|
|
1064
1207
|
Runtime Snapshot 描述已经发生的运行事实,不负责发起审批或执行流程引擎。新动作必须关联 `elementId` 与 `visitId`;`activityId` 用于精确关联工作项。
|
|
1065
1208
|
|
|
1066
1209
|
```js
|
|
@@ -1190,6 +1333,8 @@ const artifact = await viewer.exportSvg({
|
|
|
1190
1333
|
console.log(artifact.svg, artifact.width, artifact.height, artifact.warnings)
|
|
1191
1334
|
```
|
|
1192
1335
|
|
|
1336
|
+
完整 Studio 的默认导出选项放在 `config.export`;单次调用传入的选项仍用于本次导出。
|
|
1337
|
+
|
|
1193
1338
|
导出内容不受当前缩放、平移或滚动影响。移动时间线会展开全部历史动作。审批图片通过 `purpose: 'export'` 解析并嵌入 Data URL;普通附件只输出名称、MIME 和大小。第一版不导出 PNG/PDF。
|
|
1194
1339
|
|
|
1195
1340
|
## 9. 清理与生命周期
|
|
@@ -1240,11 +1385,843 @@ BPMN Nova 负责 BPMN 建模、XML 导入导出、只读展示、运行态审批
|
|
|
1240
1385
|
- [项目架构](ARCHITECTURE.md)
|
|
1241
1386
|
|
|
1242
1387
|
|
|
1388
|
+
<!-- SOURCE: docs/RUNTIME-INTEGRATION.md -->
|
|
1389
|
+
|
|
1390
|
+
# 审批轨迹接入指南
|
|
1391
|
+
|
|
1392
|
+
本文面向把真实审批记录接入 Nova 的前后端开发者。按“准备定义与实例 → 转换数据 → 挂载组件 → 更新与交互 → 验收”阅读;会签、驳回和附件规则集中在对应章节。
|
|
1393
|
+
|
|
1394
|
+
> 版本边界:Vue/React 根入口的 Runtime 类型重导出自 `0.3.5-preview` 提供。下列具名类型示例面向该版本及后续兼容版本;已安装旧版没有这些出口时,使用第 2.4 节的 Props 类型推导,不增加 Studio 直依赖。Studio 原有 Runtime 出口保持不变。
|
|
1395
|
+
|
|
1396
|
+
## 1. 准备定义与最小实例
|
|
1397
|
+
|
|
1398
|
+
### 1.1 职责与读取顺序
|
|
1399
|
+
|
|
1400
|
+
| 输入或职责 | 所有者 | 约束 |
|
|
1401
|
+
| --- | --- | --- |
|
|
1402
|
+
| BPMN XML | 宿主定义服务 | 读取实例绑定的定义版本,保留原始节点、连线和 DI |
|
|
1403
|
+
| 引擎当前状态、活动和任务历史 | 宿主引擎适配层 | 按实例查询、关联、去重,区分活动 ID 与工作项 ID |
|
|
1404
|
+
| 审批操作、回退目标、附件引用 | 宿主业务日志 | 保存真实业务事实,不从任务结束时间猜测审批决定 |
|
|
1405
|
+
| `runtime` | 宿主转换层 | 组装引擎中立的 `ProcessInstanceSnapshot` |
|
|
1406
|
+
| 图形、时间线、默认详情 | Nova | 读取 XML 与 Snapshot,派生展示,不执行审批业务 |
|
|
1407
|
+
|
|
1408
|
+
推荐由后端聚合一致的数据快照,前端只接收定义和 Snapshot。现有宿主也可以在前端执行纯转换,但引擎鉴权、查询、分页、历史保留和审批事务仍由宿主负责。`engine="flowable"` / `engine="activiti"` 选择 XML 与属性 Profile,不会自动查询引擎。
|
|
1409
|
+
|
|
1410
|
+
先按 `processInstanceId` 确认其 `processDefinitionId`,再获取该定义的 XML。实例迁移后的定义绑定也由后端明确提供。不要用流程 Key 对应的最新 XML,也不要在前端按名称重新生成节点 ID。
|
|
1411
|
+
|
|
1412
|
+
### 1.2 可复制的最小数据
|
|
1413
|
+
|
|
1414
|
+
下面保存为宿主的 `approval-data.ts`。整个指南沿用“开始 → 提交申请 → 审核 → 结束”的节点 ID。XML 是 `isExecutable="false"` 的可视化教学定义,不是包含会签表达式、回退命令和权限规则的引擎部署模板;真实接入始终使用实例自己的 XML。
|
|
1415
|
+
|
|
1416
|
+
共享 TypeScript 文件以下以 Vue 为例。React 项目把共享文件的类型导入改为 `@bpmn-nova/react`,Core 项目改为 `@bpmn-nova/studio`;每个宿主只直接依赖所选包。示例文件名是宿主代码组织建议,不是 npm 子路径。
|
|
1417
|
+
|
|
1418
|
+
```ts title="approval-data.ts"
|
|
1419
|
+
import type { ProcessInstanceSnapshot } from '@bpmn-nova/vue'
|
|
1420
|
+
|
|
1421
|
+
export const xml = `<?xml version="1.0" encoding="UTF-8"?>
|
|
1422
|
+
<bpmn:definitions xmlns:bpmn="http://www.omg.org/spec/BPMN/20100524/MODEL"
|
|
1423
|
+
xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI"
|
|
1424
|
+
xmlns:dc="http://www.omg.org/spec/DD/20100524/DC"
|
|
1425
|
+
xmlns:di="http://www.omg.org/spec/DD/20100524/DI"
|
|
1426
|
+
id="Definitions_Approval" targetNamespace="https://example.org/approval">
|
|
1427
|
+
<bpmn:process id="Process_Approval" name="申请审批" isExecutable="false">
|
|
1428
|
+
<bpmn:startEvent id="StartEvent_1" name="开始" />
|
|
1429
|
+
<bpmn:userTask id="Task_Submit" name="提交申请" />
|
|
1430
|
+
<bpmn:userTask id="Task_Review" name="审核" />
|
|
1431
|
+
<bpmn:endEvent id="EndEvent_1" name="结束" />
|
|
1432
|
+
<bpmn:sequenceFlow id="Flow_Start_Submit" sourceRef="StartEvent_1" targetRef="Task_Submit" />
|
|
1433
|
+
<bpmn:sequenceFlow id="Flow_Submit_Review" sourceRef="Task_Submit" targetRef="Task_Review" />
|
|
1434
|
+
<bpmn:sequenceFlow id="Flow_Review_End" sourceRef="Task_Review" targetRef="EndEvent_1" />
|
|
1435
|
+
</bpmn:process>
|
|
1436
|
+
<bpmndi:BPMNDiagram id="Diagram_Approval">
|
|
1437
|
+
<bpmndi:BPMNPlane id="Plane_Approval" bpmnElement="Process_Approval">
|
|
1438
|
+
<bpmndi:BPMNShape id="Shape_Start" bpmnElement="StartEvent_1">
|
|
1439
|
+
<dc:Bounds x="80" y="122" width="36" height="36" />
|
|
1440
|
+
</bpmndi:BPMNShape>
|
|
1441
|
+
<bpmndi:BPMNShape id="Shape_Submit" bpmnElement="Task_Submit">
|
|
1442
|
+
<dc:Bounds x="180" y="100" width="160" height="80" />
|
|
1443
|
+
</bpmndi:BPMNShape>
|
|
1444
|
+
<bpmndi:BPMNShape id="Shape_Review" bpmnElement="Task_Review">
|
|
1445
|
+
<dc:Bounds x="420" y="100" width="160" height="80" />
|
|
1446
|
+
</bpmndi:BPMNShape>
|
|
1447
|
+
<bpmndi:BPMNShape id="Shape_End" bpmnElement="EndEvent_1">
|
|
1448
|
+
<dc:Bounds x="650" y="122" width="36" height="36" />
|
|
1449
|
+
</bpmndi:BPMNShape>
|
|
1450
|
+
<bpmndi:BPMNEdge id="Edge_Start_Submit" bpmnElement="Flow_Start_Submit">
|
|
1451
|
+
<di:waypoint x="116" y="140" /><di:waypoint x="180" y="140" />
|
|
1452
|
+
</bpmndi:BPMNEdge>
|
|
1453
|
+
<bpmndi:BPMNEdge id="Edge_Submit_Review" bpmnElement="Flow_Submit_Review">
|
|
1454
|
+
<di:waypoint x="340" y="140" /><di:waypoint x="420" y="140" />
|
|
1455
|
+
</bpmndi:BPMNEdge>
|
|
1456
|
+
<bpmndi:BPMNEdge id="Edge_Review_End" bpmnElement="Flow_Review_End">
|
|
1457
|
+
<di:waypoint x="580" y="140" /><di:waypoint x="650" y="140" />
|
|
1458
|
+
</bpmndi:BPMNEdge>
|
|
1459
|
+
</bpmndi:BPMNPlane>
|
|
1460
|
+
</bpmndi:BPMNDiagram>
|
|
1461
|
+
</bpmn:definitions>`
|
|
1462
|
+
|
|
1463
|
+
export const runtime: ProcessInstanceSnapshot = {
|
|
1464
|
+
processInstanceId: 'PI-1001',
|
|
1465
|
+
status: 'running',
|
|
1466
|
+
activities: [
|
|
1467
|
+
{ id: 'activity:h-start', elementId: 'StartEvent_1', visitId: 'visit-start-1', status: 'completed', startTime: '2026-09-05T09:00:00+08:00', endTime: '2026-09-05T09:00:00+08:00' },
|
|
1468
|
+
{ id: 'task:t-submit-1', elementId: 'Task_Submit', visitId: 'visit-submit-1', status: 'completed', startTime: '2026-09-05T09:00:01+08:00', endTime: '2026-09-05T09:05:00+08:00', participant: { id: 'u-applicant', name: '申请人' } },
|
|
1469
|
+
{ id: 'task:t-review-1', elementId: 'Task_Review', visitId: 'visit-review-1', status: 'active', startTime: '2026-09-05T09:05:01+08:00', participant: { id: 'u-li', name: '李经理' } },
|
|
1470
|
+
],
|
|
1471
|
+
visitedEdges: ['Flow_Start_Submit', 'Flow_Submit_Review'],
|
|
1472
|
+
}
|
|
1473
|
+
```
|
|
1474
|
+
|
|
1475
|
+
最小 Snapshot 不要求 `actions`,所以这里只能展示工作项状态和处理人,不能据此声称“已经同意”。第 4 节再补充真实操作。
|
|
1476
|
+
|
|
1477
|
+
最小 Vue 组件保存为 `ApprovalPreview.vue`,仅在浏览器挂载;Nuxt 等 SSR 宿主使用自己的客户端边界:
|
|
1478
|
+
|
|
1479
|
+
```vue title="ApprovalPreview.vue"
|
|
1480
|
+
<script setup lang="ts">
|
|
1481
|
+
import { BpmnViewer } from '@bpmn-nova/vue'
|
|
1482
|
+
import '@bpmn-nova/vue/styles.css'
|
|
1483
|
+
import { xml, runtime } from './approval-data'
|
|
1484
|
+
</script>
|
|
1485
|
+
|
|
1486
|
+
<template>
|
|
1487
|
+
<div style="height: 640px; min-height: 320px">
|
|
1488
|
+
<BpmnViewer :xml="xml" :runtime="runtime" engine="flowable" projection="approval" />
|
|
1489
|
+
</div>
|
|
1490
|
+
</template>
|
|
1491
|
+
```
|
|
1492
|
+
|
|
1493
|
+
完成条件:审核节点显示处理中和李经理;切换 `standard` 能看到全部四个节点;没有请求业务后端的隐式行为。父级隐藏或高度为零时应先恢复布局,再调用公开 `fitView()`。
|
|
1494
|
+
|
|
1495
|
+
## 2. 数据契约与身份关联
|
|
1496
|
+
|
|
1497
|
+
### 2.1 Snapshot 与记录
|
|
1498
|
+
|
|
1499
|
+
| 字段 | 类型与必填性 | 语义 |
|
|
1500
|
+
| --- | --- | --- |
|
|
1501
|
+
| `processInstanceId` | 必填 `string` | 一个真实实例的稳定 ID |
|
|
1502
|
+
| `status` | 必填 `running / completed / suspended / terminated` | 实例状态;未发起不是上述状态之一 |
|
|
1503
|
+
| `activities` | 必填 `ActivityInstance[]` | 已发生的活动执行或工作项,包含当前记录与历史 |
|
|
1504
|
+
| `visitedEdges` | 必填 `string[]`,可为空 | 已确认曾经过的 BPMN 连线 ID 汇总,不表示当前全部有效 |
|
|
1505
|
+
| `actions` | 可选 `RuntimeApprovalAction[]` | 不可变的业务操作事实 |
|
|
1506
|
+
| `transitions` | 可选 `RuntimeTransition[]` | 运行时跳转,独立于 BPMN Sequence Flow |
|
|
1507
|
+
| `edgeVisits` | 可选 `RuntimeEdgeVisit[]` | 每次经过连线的独立记录,支持重复经过及失效历史 |
|
|
1508
|
+
|
|
1509
|
+
| 记录 | 必填字段 | 常用可选字段 |
|
|
1510
|
+
| --- | --- | --- |
|
|
1511
|
+
| `ActivityInstance` | `id`、`elementId`、`status` | `visitId`、起止时间、`participant`、会签元数据 |
|
|
1512
|
+
| `RuntimeApprovalAction` | `id`、`type`、`elementId`、`visitId`、`occurredAt` | `activityId`、`actor`、`targets`、`targetElementId`、`content` |
|
|
1513
|
+
| `RuntimeTransition` | `id`、`type`、`sourceElementId` | 来源工作项、目标节点、时间、`actionId`、状态和失效范围 |
|
|
1514
|
+
| `RuntimeEdgeVisit` | `id`、`edgeId` | `visitId`、`occurredAt`、`status: effective / superseded` |
|
|
1515
|
+
|
|
1516
|
+
新接入应完整提供 `visitId` 和带时区的 ISO 时间。虽有兼容回退,缺失时间或关联 ID 不能保证多轮审批顺序正确。时间使用 `2026-09-05T09:05:00+08:00` 或 UTC `Z`,不要依赖浏览器解析无时区文本。
|
|
1517
|
+
|
|
1518
|
+
活动状态只接受 `active / completed / failed / cancelled / skipped`;`idle` 是没有工作项时的派生状态,`rejected` 是展示状态,两者都不是 `ActivityInstance.status` 的合法值。实例挂起不等于所有工作项取消,或签剩余任务被取消也不等于流程终止。
|
|
1519
|
+
|
|
1520
|
+
### 2.2 四类 ID
|
|
1521
|
+
|
|
1522
|
+
| ID | 示例 | 关联规则 |
|
|
1523
|
+
| --- | --- | --- |
|
|
1524
|
+
| BPMN 元素 | `Task_Review` | 来自实例 XML;`elementId` 与 Transition 端点引用它 |
|
|
1525
|
+
| 工作项 | `task:t-review-1` | 一次执行;Action `activityId`、Transition `sourceActivityId` 引用它 |
|
|
1526
|
+
| 节点到达 | `visit-review-1` | 同一节点本次到达;会签工作项共享,重入创建新 ID |
|
|
1527
|
+
| 操作 / 跳转 / 连线经过 | `op-approve-1` / `move-1` / `ev-1` | 每条事实稳定且唯一,不能在轮询时重新随机生成 |
|
|
1528
|
+
|
|
1529
|
+
`visitId` 建议在实例内唯一,并始终与同一个 BPMN 节点关联。`executionId`、task ID、时间戳或数组序号都不能普遍替代会签访问 ID;宿主必须保存或可靠关联每次到达及多实例作用域。没有可信关联时,应报告数据缺口,而不是合并同节点的全部历史。
|
|
1530
|
+
|
|
1531
|
+
会签使用 `approvalMode: 'all'`,或签使用 `'any'`,单人使用 `'single'`。多人参与不会自动变成会签。`multiInstanceMode` 区分 `parallel / sequential`,`totalInstances` 是本轮总人数,`requiredInstances` 是业务完成门槛。Nova 展示这些事实,不在达到门槛时替引擎结束或取消任务;或签完成后应传入其余工作项的真实终态。
|
|
1532
|
+
|
|
1533
|
+
### 2.3 操作、回退与历史
|
|
1534
|
+
|
|
1535
|
+
`actions` 支持 `submit / approve / reject / return / add-sign / transfer / delegate / withdraw / comment / skip` 及宿主自定义类型。转办可以只有操作记录,不需要制造一条 BPMN 边。`actor` 是实际操作者,不一定是任务最终的 `assignee`。
|
|
1536
|
+
|
|
1537
|
+
驳回路径由 `transitions` 表达,`actionId` 关联同一次业务操作,`sourceActivityId` 精确关联工作项。仅增加 `type: 'reject'` 的 Action 不会自动画出回退路径。结束的驳回工作项仍可为 `completed`;配套 reject Transition 会派生“已驳回”展示。
|
|
1538
|
+
|
|
1539
|
+
当前 `invalidatedActivityIds` 名称保留兼容,但其值是 **BPMN 节点 ID**,例如 `['Task_Review']`,不是 `['task:t-review-1']`。`invalidatedEdgeIds` 同样是 BPMN 连线 ID。两者应共同明确给出;任一数组存在就属于显式范围,不再自动补推另一项。缺少范围时仅对唯一、同作用域的已访问路径做兼容推断,不能覆盖所有并行、跨作用域和回环情况。
|
|
1540
|
+
|
|
1541
|
+
失效只影响当前路径展示,不删除历史,也不修改 XML。再次经过的边应新增带更晚时间的 `edgeVisits`,旧记录继续保留;宿主确认目标本轮处理完成后把回退 Transition 更新为 `resolved`,可同时提供 `resolvedAt` / `resolvedByActivityId`。操作日志本身不随状态更新而被改写。
|
|
1542
|
+
|
|
1543
|
+
旧 `ActivityInstance.comment/outcome` 和 `RuntimeTransition.comment` 会生成兼容动作。新接入优先显式 `actions`:填入 `activityId` 可抑制同工作项的兼容动作,填入 Transition `actionId` 可避免同次跳转再次生成动作。`normalizeRuntime()` 只是兼容归一化,不是业务事实校验器。
|
|
1544
|
+
|
|
1545
|
+
### 2.4 公共类型与已发布版本兼容
|
|
1546
|
+
|
|
1547
|
+
自 `0.3.5-preview` 起,Vue/React 根入口显式重导出以下类型,均为 `export type`,没有同名 JavaScript 值:
|
|
1548
|
+
|
|
1549
|
+
- `ProcessInstanceSnapshot`、`ActivityInstance`、`ActivityStatus`、`RuntimeParticipant`。
|
|
1550
|
+
- `RuntimeApprovalAction`、`RuntimeApprovalActionType`、`RuntimeApprovalContent`、`RuntimeApprovalContentBlock`、`RuntimeAssetRef`。
|
|
1551
|
+
- `RuntimeTransition`、`RuntimeEdgeVisit`。
|
|
1552
|
+
- `RuntimeTraceClickEvent`、`RuntimeAssetResolver`、`RuntimeAssetPurpose`。
|
|
1553
|
+
|
|
1554
|
+
已安装包尚未提供这些具名出口时,可从现有 Props 推导,不增加依赖:
|
|
1555
|
+
|
|
1556
|
+
```ts title="runtime-compat.ts"
|
|
1557
|
+
import type { BpmnStudioProps } from '@bpmn-nova/vue'
|
|
1558
|
+
|
|
1559
|
+
export type ProcessInstanceSnapshot = NonNullable<BpmnStudioProps['runtime']>
|
|
1560
|
+
export type ActivityInstance = ProcessInstanceSnapshot['activities'][number]
|
|
1561
|
+
export type RuntimeApprovalAction = NonNullable<ProcessInstanceSnapshot['actions']>[number]
|
|
1562
|
+
export type RuntimeTransition = NonNullable<ProcessInstanceSnapshot['transitions']>[number]
|
|
1563
|
+
export type RuntimeEdgeVisit = NonNullable<ProcessInstanceSnapshot['edgeVisits']>[number]
|
|
1564
|
+
```
|
|
1565
|
+
|
|
1566
|
+
React 使用相同写法并改为 React 包。其他结构可继续按属性推导;不要从源码相对路径或传递依赖导入。下文 Studio `config` 示例要求包含 Config 功能的构建;旧构建沿用同义的顶层 Viewer 配置,不影响 Snapshot 格式。
|
|
1567
|
+
|
|
1568
|
+
## 3. Flowable / Activiti 映射参考
|
|
1569
|
+
|
|
1570
|
+
### 3.1 官方资料与边界
|
|
1571
|
+
|
|
1572
|
+
资料核对日期:2026-09-05。字段表依据 [Flowable HistoricActivityInstance(页面标识 8.0.0)](https://www.flowable.com/open-source/docs/all-javadocs/org/flowable/engine/history/HistoricActivityInstance.html) 与 [Activiti 6.0.0 HistoricActivityInstance](https://www.activiti.org/javadocs/6.latest/org/activiti/engine/history/HistoricActivityInstance.html)。Flowable 链接是滚动文档;宿主使用其他版本时应对照实际依赖的 API,本文不声明 Activiti 7/Cloud 或所有引擎版本已经联调通过。
|
|
1573
|
+
|
|
1574
|
+
| 引擎活动字段 | 宿主映射 |
|
|
1575
|
+
| --- | --- |
|
|
1576
|
+
| `getProcessInstanceId()` / `getProcessDefinitionId()` | 校验实例归属及绑定的定义版本 |
|
|
1577
|
+
| `getActivityId()` | Nova `elementId`,来自 BPMN 定义 |
|
|
1578
|
+
| `getId()` | 历史活动实例 ID;非人工工作项可据此建立稳定记录 ID |
|
|
1579
|
+
| `getTaskId()` | 关联人工任务;用同一 task ID 合并任务与活动两种视角 |
|
|
1580
|
+
| `getExecutionId()` | 执行关联线索,不默认当作 `visitId` |
|
|
1581
|
+
| `getStartTime()` / `getEndTime()` | 转换为带时区的开始和结束时间 |
|
|
1582
|
+
| `getAssignee()` | 处理人标识;显示名称另向宿主人员服务解析 |
|
|
1583
|
+
| `getDeleteReason()` | 状态判断的输入之一,不通用等价于“驳回” |
|
|
1584
|
+
|
|
1585
|
+
Flowable 历史记录包含进行中与已结束实例,因此实时任务和历史查询结果应关联去重,而不是直接拼接。历史级别影响可获得的记录:`none` 没有可用历史;`activity` 保留活动级信息;`audit` / `full` 提供更丰富记录。历史清理也可能移除旧记录。接入前应记录宿主实际配置和保留策略,而不是只检查接口返回成功。[Flowable History](https://www.flowable.com/open-source/docs/bpmn/ch10-History)、[Activiti 6 History](https://www.activiti.org/userguide/6.latest/#history)
|
|
1586
|
+
|
|
1587
|
+
上述字段并不构成完整业务审计契约。`approve/reject`、回退目标、会签访问身份、完成门槛及可靠的连线经过记录必须结合宿主已有日志或引擎扩展。数据采集不足时,在宿主页面说明“轨迹不完整”;即使历史级别为 `full`,也不要把自定义审批业务含义视为自动可得。
|
|
1588
|
+
|
|
1589
|
+
### 3.2 宿主 DTO 与纯转换
|
|
1590
|
+
|
|
1591
|
+
以下 `runtime-adapter.ts` 是可复制的**宿主参考代码**,不是 Nova 导出,也不是某个引擎的 REST Schema。接口边界先完成鉴权、分页、原始 JSON 校验、人员名称解析及一致性读取,再交给此函数。`revision` 是宿主为同一工作项提供的单调版本,不能用数组位置生成。
|
|
1592
|
+
|
|
1593
|
+
人工任务统一使用 `{ kind: 'task', id: taskId }`,活动历史中的人工任务也使用相同键;非任务活动使用 `{ kind: 'activity', id: historicActivityId }`。不要同时把同一人工任务映射成一个 task 记录和一个 activity 记录。若历史暂时没有对应 task ID,应先完成关联或报告缺失,不能换一个新 ID 填入。
|
|
1594
|
+
|
|
1595
|
+
```ts title="runtime-adapter.ts"
|
|
1596
|
+
import type {
|
|
1597
|
+
ActivityInstance, ProcessInstanceSnapshot, RuntimeApprovalAction,
|
|
1598
|
+
RuntimeEdgeVisit, RuntimeTransition,
|
|
1599
|
+
} from '@bpmn-nova/vue'
|
|
1600
|
+
|
|
1601
|
+
export interface WorkItemKey { kind: 'task' | 'activity'; id: string }
|
|
1602
|
+
export interface HostActivity {
|
|
1603
|
+
key: WorkItemKey
|
|
1604
|
+
revision: number
|
|
1605
|
+
nodeId: string
|
|
1606
|
+
visitId: string
|
|
1607
|
+
state: ActivityInstance['status']
|
|
1608
|
+
startedAt: string
|
|
1609
|
+
endedAt?: string
|
|
1610
|
+
participant?: ActivityInstance['participant']
|
|
1611
|
+
approval?: Pick<ActivityInstance,
|
|
1612
|
+
'approvalMode' | 'multiInstanceId' | 'multiInstanceMode' |
|
|
1613
|
+
'totalInstances' | 'requiredInstances'>
|
|
1614
|
+
}
|
|
1615
|
+
export interface HostOperation extends Omit<RuntimeApprovalAction, 'activityId'> {
|
|
1616
|
+
workItem: WorkItemKey
|
|
1617
|
+
}
|
|
1618
|
+
export interface HostMovement extends Omit<RuntimeTransition,
|
|
1619
|
+
'sourceActivityId' | 'invalidatedActivityIds' | 'actionId'> {
|
|
1620
|
+
sourceWorkItem: WorkItemKey
|
|
1621
|
+
actionId: string
|
|
1622
|
+
invalidatedElementIds: string[]
|
|
1623
|
+
}
|
|
1624
|
+
export interface HostTraceSource {
|
|
1625
|
+
processInstanceId: string
|
|
1626
|
+
status: ProcessInstanceSnapshot['status']
|
|
1627
|
+
historicalActivities: HostActivity[]
|
|
1628
|
+
currentActivities: HostActivity[]
|
|
1629
|
+
operations: HostOperation[]
|
|
1630
|
+
movements: HostMovement[]
|
|
1631
|
+
visitedEdgeIds: string[]
|
|
1632
|
+
edgeVisits: RuntimeEdgeVisit[]
|
|
1633
|
+
coverage: { activities: 'complete' | 'partial'; actions: 'complete' | 'partial'; edges: 'complete' | 'partial' }
|
|
1634
|
+
}
|
|
1635
|
+
export interface ModelIds {
|
|
1636
|
+
nodes: ReadonlySet<string>
|
|
1637
|
+
edges: ReadonlySet<string>
|
|
1638
|
+
}
|
|
1639
|
+
|
|
1640
|
+
export function workItemId(key: WorkItemKey): string {
|
|
1641
|
+
if (!key.id.trim()) throw new Error('缺少工作项来源 ID')
|
|
1642
|
+
return `${key.kind}:${key.id}`
|
|
1643
|
+
}
|
|
1644
|
+
|
|
1645
|
+
export function toNovaRuntime(source: HostTraceSource, ids: ModelIds): ProcessInstanceSnapshot {
|
|
1646
|
+
const requireId = (set: ReadonlySet<string>, id: string, field: string) => {
|
|
1647
|
+
if (!set.has(id)) throw new Error(`${field} 无法关联: ${id}`)
|
|
1648
|
+
}
|
|
1649
|
+
const requireTime = (time: string) => {
|
|
1650
|
+
if (!/T.*(?:Z|[+-]\d{2}:\d{2})$/.test(time) || !Number.isFinite(Date.parse(time))) {
|
|
1651
|
+
throw new Error(`需要带时区的 ISO 时间: ${time}`)
|
|
1652
|
+
}
|
|
1653
|
+
}
|
|
1654
|
+
if (!source.processInstanceId.trim()) throw new Error('缺少 processInstanceId')
|
|
1655
|
+
const records = new Map<string, { revision: number; activity: ActivityInstance }>()
|
|
1656
|
+
for (const row of [...source.historicalActivities, ...source.currentActivities]) {
|
|
1657
|
+
requireId(ids.nodes, row.nodeId, 'nodeId')
|
|
1658
|
+
if (!row.visitId.trim()) throw new Error(`缺少 visitId: ${row.nodeId}`)
|
|
1659
|
+
if (!Number.isSafeInteger(row.revision) || row.revision < 0) throw new Error('无效工作项 revision')
|
|
1660
|
+
requireTime(row.startedAt)
|
|
1661
|
+
if (row.endedAt) {
|
|
1662
|
+
requireTime(row.endedAt)
|
|
1663
|
+
if (Date.parse(row.endedAt) < Date.parse(row.startedAt)) throw new Error('工作项结束早于开始')
|
|
1664
|
+
}
|
|
1665
|
+
const activity: ActivityInstance = {
|
|
1666
|
+
id: workItemId(row.key), elementId: row.nodeId, visitId: row.visitId,
|
|
1667
|
+
status: row.state, startTime: row.startedAt, endTime: row.endedAt,
|
|
1668
|
+
participant: row.participant ? {
|
|
1669
|
+
id: row.participant.id, name: row.participant.name, avatarUrl: row.participant.avatarUrl,
|
|
1670
|
+
} : undefined,
|
|
1671
|
+
approvalMode: row.approval?.approvalMode, multiInstanceId: row.approval?.multiInstanceId,
|
|
1672
|
+
multiInstanceMode: row.approval?.multiInstanceMode,
|
|
1673
|
+
totalInstances: row.approval?.totalInstances, requiredInstances: row.approval?.requiredInstances,
|
|
1674
|
+
}
|
|
1675
|
+
const previous = records.get(activity.id)
|
|
1676
|
+
if (previous && (previous.activity.elementId !== activity.elementId || previous.activity.visitId !== activity.visitId)) {
|
|
1677
|
+
throw new Error(`工作项身份冲突: ${activity.id}`)
|
|
1678
|
+
}
|
|
1679
|
+
if (previous && previous.revision === row.revision && JSON.stringify(previous.activity) !== JSON.stringify(activity)) {
|
|
1680
|
+
throw new Error(`相同 revision 的记录冲突: ${activity.id}`)
|
|
1681
|
+
}
|
|
1682
|
+
if (!previous || row.revision > previous.revision) records.set(activity.id, { revision: row.revision, activity })
|
|
1683
|
+
}
|
|
1684
|
+
const activities = [...records.values()].map(({ activity }) => activity)
|
|
1685
|
+
.sort((a, b) => Date.parse(a.startTime!) - Date.parse(b.startTime!) || a.id.localeCompare(b.id))
|
|
1686
|
+
const visits = new Map<string, string>()
|
|
1687
|
+
for (const activity of activities) {
|
|
1688
|
+
const nodeId = visits.get(activity.visitId!)
|
|
1689
|
+
if (nodeId && nodeId !== activity.elementId) throw new Error(`visitId 跨节点复用: ${activity.visitId}`)
|
|
1690
|
+
visits.set(activity.visitId!, activity.elementId)
|
|
1691
|
+
}
|
|
1692
|
+
const actionIds = new Set<string>()
|
|
1693
|
+
const actions: RuntimeApprovalAction[] = source.operations.map(({ workItem, ...operation }) => {
|
|
1694
|
+
if (!operation.id.trim() || actionIds.has(operation.id)) throw new Error(`操作 ID 缺失或重复: ${operation.id}`)
|
|
1695
|
+
actionIds.add(operation.id)
|
|
1696
|
+
requireTime(operation.occurredAt)
|
|
1697
|
+
const record = records.get(workItemId(workItem))?.activity
|
|
1698
|
+
if (!record || record.elementId !== operation.elementId || record.visitId !== operation.visitId) {
|
|
1699
|
+
throw new Error(`操作与工作项不匹配: ${operation.id}`)
|
|
1700
|
+
}
|
|
1701
|
+
if (operation.targetElementId) requireId(ids.nodes, operation.targetElementId, 'targetElementId')
|
|
1702
|
+
return { ...structuredClone(operation), activityId: record.id }
|
|
1703
|
+
}).sort((a, b) => Date.parse(a.occurredAt) - Date.parse(b.occurredAt) || a.id.localeCompare(b.id))
|
|
1704
|
+
const movementIds = new Set<string>()
|
|
1705
|
+
const transitions: RuntimeTransition[] = source.movements.map(({ sourceWorkItem, invalidatedElementIds, ...movement }) => {
|
|
1706
|
+
if (!movement.id.trim() || movementIds.has(movement.id)) throw new Error(`跳转 ID 缺失或重复: ${movement.id}`)
|
|
1707
|
+
movementIds.add(movement.id)
|
|
1708
|
+
const record = records.get(workItemId(sourceWorkItem))?.activity
|
|
1709
|
+
const action = actions.find((item) => item.id === movement.actionId)
|
|
1710
|
+
if (!record || record.elementId !== movement.sourceElementId || !action || action.activityId !== record.id
|
|
1711
|
+
|| action.type !== movement.type || action.targetElementId !== movement.targetElementId) {
|
|
1712
|
+
throw new Error(`跳转与操作不匹配: ${movement.id}`)
|
|
1713
|
+
}
|
|
1714
|
+
if (movement.occurredAt) requireTime(movement.occurredAt)
|
|
1715
|
+
if (movement.resolvedAt) requireTime(movement.resolvedAt)
|
|
1716
|
+
if (movement.resolvedByActivityId && !records.has(movement.resolvedByActivityId)) throw new Error('跳转完成工作项不存在')
|
|
1717
|
+
invalidatedElementIds.forEach((id) => requireId(ids.nodes, id, 'invalidatedElementIds'))
|
|
1718
|
+
;(movement.invalidatedEdgeIds ?? []).forEach((id) => requireId(ids.edges, id, 'invalidatedEdgeIds'))
|
|
1719
|
+
return { ...structuredClone(movement), sourceActivityId: record.id, invalidatedActivityIds: [...invalidatedElementIds] }
|
|
1720
|
+
})
|
|
1721
|
+
const edgeVisitIds = new Set<string>()
|
|
1722
|
+
for (const visit of source.edgeVisits) {
|
|
1723
|
+
if (!visit.id.trim() || edgeVisitIds.has(visit.id)) throw new Error(`连线经过 ID 缺失或重复: ${visit.id}`)
|
|
1724
|
+
edgeVisitIds.add(visit.id)
|
|
1725
|
+
requireId(ids.edges, visit.edgeId, 'edgeId')
|
|
1726
|
+
if (visit.occurredAt) requireTime(visit.occurredAt)
|
|
1727
|
+
}
|
|
1728
|
+
const visitedEdges = [...new Set([...source.visitedEdgeIds, ...source.edgeVisits.map((visit) => visit.edgeId)])]
|
|
1729
|
+
visitedEdges.forEach((id) => requireId(ids.edges, id, 'visitedEdges'))
|
|
1730
|
+
return {
|
|
1731
|
+
processInstanceId: source.processInstanceId, status: source.status, activities,
|
|
1732
|
+
actions, transitions, visitedEdges, edgeVisits: structuredClone(source.edgeVisits),
|
|
1733
|
+
}
|
|
1734
|
+
}
|
|
1735
|
+
```
|
|
1736
|
+
|
|
1737
|
+
此函数只去重同一工作项的不同读取视角,不合并不同工作项或不同访问轮次。相同版本冲突直接报错;不会用旧活动记录覆盖较新任务记录。它是已校验 DTO 的教学转换层,不是通用 JSON Schema 校验器,也不替宿主推断审批状态、轮次或跳转范围。
|
|
1738
|
+
|
|
1739
|
+
参考 DTO 针对本文的工作项审批场景,要求每个操作都关联已采集工作项。公共 Action 接口的 `activityId` 实际可选;宿主存在只有节点与访问身份的操作时,应保留真实 `elementId / visitId` 并按该接口扩展转换层,而不是伪造工作项来满足本例。
|
|
1740
|
+
|
|
1741
|
+
`coverage` 是宿主页面的完整性提示,不属于 Nova Snapshot。示例中的 `ModelIds` 从同版本解析模型生成:节点集合取 `model.nodes[].id`,连线集合取 `model.edges[].id`。身份不匹配时应保留上次成功显示的数据并提示错误,而不是改 XML ID 来迁就日志。
|
|
1742
|
+
|
|
1743
|
+
## 4. 同一流程的完整业务场景
|
|
1744
|
+
|
|
1745
|
+
以下 `runtime-scenarios.ts` 与第 3 节转换器一起使用,没有省略的输入数组。公共事实只定义一次,各场景对象独立组装,`scenarioSnapshots` 保存每份输入对应的完整 Snapshot;`JSON.stringify(scenarioSnapshots.reject.runtime, null, 2)` 可查看驳回场景的完整输出。
|
|
1746
|
+
|
|
1747
|
+
这些是教学数据,不是 Playground 的真实接口响应。“单人完成”“或签完成”是不同实例分支;“驳回 → 重新提交”则是同一实例的后续快照。会签/或签示例只说明相同可视节点如何接收不同工作项,实际引擎模型及宿主审批策略必须与事实一致。
|
|
1748
|
+
|
|
1749
|
+
```ts title="runtime-scenarios.ts"
|
|
1750
|
+
import type { RuntimeEdgeVisit } from '@bpmn-nova/vue'
|
|
1751
|
+
import { toNovaRuntime } from './runtime-adapter'
|
|
1752
|
+
import type { HostActivity, HostOperation, HostTraceSource, ModelIds } from './runtime-adapter'
|
|
1753
|
+
|
|
1754
|
+
// 教学 XML 的身份集合。真实接入从实例对应的解析模型生成。
|
|
1755
|
+
const ids: ModelIds = {
|
|
1756
|
+
nodes: new Set(['StartEvent_1', 'Task_Submit', 'Task_Review', 'EndEvent_1']),
|
|
1757
|
+
edges: new Set(['Flow_Start_Submit', 'Flow_Submit_Review', 'Flow_Review_End']),
|
|
1758
|
+
}
|
|
1759
|
+
const applicant = { id: 'u-applicant', name: '申请人' }
|
|
1760
|
+
const manager = { id: 'u-li', name: '李经理' }
|
|
1761
|
+
const start: HostActivity = {
|
|
1762
|
+
key: { kind: 'activity', id: 'h-start' }, revision: 1,
|
|
1763
|
+
nodeId: 'StartEvent_1', visitId: 'visit-start-1', state: 'completed',
|
|
1764
|
+
startedAt: '2026-09-05T09:00:00+08:00', endedAt: '2026-09-05T09:00:00+08:00',
|
|
1765
|
+
}
|
|
1766
|
+
const submit: HostActivity = {
|
|
1767
|
+
key: { kind: 'task', id: 't-submit-1' }, revision: 2,
|
|
1768
|
+
nodeId: 'Task_Submit', visitId: 'visit-submit-1', state: 'completed', participant: applicant,
|
|
1769
|
+
startedAt: '2026-09-05T09:00:01+08:00', endedAt: '2026-09-05T09:05:00+08:00',
|
|
1770
|
+
}
|
|
1771
|
+
const review: HostActivity = {
|
|
1772
|
+
key: { kind: 'task', id: 't-review-1' }, revision: 2,
|
|
1773
|
+
nodeId: 'Task_Review', visitId: 'visit-review-1', state: 'active', participant: manager,
|
|
1774
|
+
startedAt: '2026-09-05T09:05:01+08:00', approval: { approvalMode: 'single' },
|
|
1775
|
+
}
|
|
1776
|
+
const submitAction: HostOperation = {
|
|
1777
|
+
id: 'op-submit-1', type: 'submit', workItem: submit.key,
|
|
1778
|
+
elementId: submit.nodeId, visitId: submit.visitId, actor: applicant,
|
|
1779
|
+
occurredAt: '2026-09-05T09:05:00+08:00', content: { plainText: '提交申请。' },
|
|
1780
|
+
}
|
|
1781
|
+
const beforeReview: RuntimeEdgeVisit[] = [
|
|
1782
|
+
{ id: 'ev-1', edgeId: 'Flow_Start_Submit', occurredAt: '2026-09-05T09:00:01+08:00', status: 'effective' },
|
|
1783
|
+
{ id: 'ev-2', edgeId: 'Flow_Submit_Review', occurredAt: '2026-09-05T09:05:01+08:00', status: 'effective' },
|
|
1784
|
+
]
|
|
1785
|
+
|
|
1786
|
+
export const singleInput: HostTraceSource = {
|
|
1787
|
+
processInstanceId: 'PI-1001', status: 'running',
|
|
1788
|
+
historicalActivities: [start, submit, { ...review, revision: 1, participant: undefined }],
|
|
1789
|
+
currentActivities: [review], operations: [submitAction], movements: [],
|
|
1790
|
+
visitedEdgeIds: ['Flow_Start_Submit', 'Flow_Submit_Review'], edgeVisits: beforeReview,
|
|
1791
|
+
coverage: { activities: 'complete', actions: 'complete', edges: 'complete' },
|
|
1792
|
+
}
|
|
1793
|
+
export const waitingInput: HostTraceSource = {
|
|
1794
|
+
...singleInput, processInstanceId: 'PI-WAITING',
|
|
1795
|
+
currentActivities: [{ ...review, revision: 3, participant: undefined }],
|
|
1796
|
+
}
|
|
1797
|
+
|
|
1798
|
+
const reviewed: HostActivity = {
|
|
1799
|
+
...review, revision: 3, state: 'completed', endedAt: '2026-09-05T09:10:00+08:00',
|
|
1800
|
+
}
|
|
1801
|
+
const approveAction: HostOperation = {
|
|
1802
|
+
id: 'op-approve-1', type: 'approve', workItem: review.key,
|
|
1803
|
+
elementId: review.nodeId, visitId: review.visitId, actor: manager,
|
|
1804
|
+
occurredAt: '2026-09-05T09:10:00+08:00', content: { plainText: '同意。' },
|
|
1805
|
+
}
|
|
1806
|
+
const end: HostActivity = {
|
|
1807
|
+
key: { kind: 'activity', id: 'h-end' }, revision: 1,
|
|
1808
|
+
nodeId: 'EndEvent_1', visitId: 'visit-end-1', state: 'completed',
|
|
1809
|
+
startedAt: '2026-09-05T09:10:01+08:00', endedAt: '2026-09-05T09:10:01+08:00',
|
|
1810
|
+
}
|
|
1811
|
+
const toEnd: RuntimeEdgeVisit = {
|
|
1812
|
+
id: 'ev-end-1', edgeId: 'Flow_Review_End', occurredAt: '2026-09-05T09:10:01+08:00', status: 'effective',
|
|
1813
|
+
}
|
|
1814
|
+
export const completedInput: HostTraceSource = {
|
|
1815
|
+
...singleInput, processInstanceId: 'PI-COMPLETED', status: 'completed',
|
|
1816
|
+
historicalActivities: [start, submit, reviewed, end], currentActivities: [],
|
|
1817
|
+
operations: [submitAction, approveAction], edgeVisits: [...beforeReview, toEnd],
|
|
1818
|
+
}
|
|
1819
|
+
|
|
1820
|
+
const allApproval: NonNullable<HostActivity['approval']> = {
|
|
1821
|
+
approvalMode: 'all', multiInstanceMode: 'parallel', multiInstanceId: 'mi-review-1',
|
|
1822
|
+
totalInstances: 3, requiredInstances: 3,
|
|
1823
|
+
}
|
|
1824
|
+
const reviewerTwo: HostActivity = {
|
|
1825
|
+
...review, key: { kind: 'task', id: 't-review-2' }, participant: { id: 'u-wang', name: '王经理' }, approval: allApproval,
|
|
1826
|
+
}
|
|
1827
|
+
const reviewerThree: HostActivity = {
|
|
1828
|
+
...review, key: { kind: 'task', id: 't-review-3' }, participant: { id: 'u-zhao', name: '赵经理' }, approval: allApproval,
|
|
1829
|
+
}
|
|
1830
|
+
export const countersignInput: HostTraceSource = {
|
|
1831
|
+
...singleInput, processInstanceId: 'PI-ALL',
|
|
1832
|
+
historicalActivities: [start, submit, { ...reviewed, approval: allApproval }],
|
|
1833
|
+
currentActivities: [reviewerTwo, reviewerThree], operations: [submitAction, approveAction],
|
|
1834
|
+
}
|
|
1835
|
+
const anyApproval: NonNullable<HostActivity['approval']> = { ...allApproval, approvalMode: 'any', requiredInstances: 1 }
|
|
1836
|
+
export const anySignInput: HostTraceSource = {
|
|
1837
|
+
...singleInput, processInstanceId: 'PI-ANY', status: 'completed',
|
|
1838
|
+
historicalActivities: [
|
|
1839
|
+
start, submit, { ...reviewed, approval: anyApproval },
|
|
1840
|
+
{ ...reviewerTwo, revision: 3, state: 'cancelled', endedAt: '2026-09-05T09:10:00+08:00', approval: anyApproval },
|
|
1841
|
+
{ ...reviewerThree, revision: 3, state: 'cancelled', endedAt: '2026-09-05T09:10:00+08:00', approval: anyApproval },
|
|
1842
|
+
end,
|
|
1843
|
+
],
|
|
1844
|
+
currentActivities: [], operations: [submitAction, approveAction], edgeVisits: [...beforeReview, toEnd],
|
|
1845
|
+
}
|
|
1846
|
+
|
|
1847
|
+
const submitAgain: HostActivity = {
|
|
1848
|
+
...submit, key: { kind: 'task', id: 't-submit-2' }, revision: 1,
|
|
1849
|
+
visitId: 'visit-submit-2', state: 'active', startedAt: '2026-09-05T09:10:01+08:00', endedAt: undefined,
|
|
1850
|
+
}
|
|
1851
|
+
const rejectAction: HostOperation = {
|
|
1852
|
+
id: 'op-reject-1', type: 'reject', workItem: review.key,
|
|
1853
|
+
elementId: review.nodeId, visitId: review.visitId, actor: manager,
|
|
1854
|
+
targetElementId: 'Task_Submit', occurredAt: '2026-09-05T09:10:00+08:00',
|
|
1855
|
+
content: { plainText: '请补充材料后重新提交。' },
|
|
1856
|
+
}
|
|
1857
|
+
export const rejectInput: HostTraceSource = {
|
|
1858
|
+
...singleInput,
|
|
1859
|
+
historicalActivities: [start, submit, reviewed], currentActivities: [submitAgain],
|
|
1860
|
+
operations: [submitAction, rejectAction],
|
|
1861
|
+
movements: [{
|
|
1862
|
+
id: 'move-reject-1', type: 'reject', actionId: rejectAction.id, sourceWorkItem: review.key,
|
|
1863
|
+
sourceElementId: 'Task_Review', targetElementId: 'Task_Submit', operator: manager.name,
|
|
1864
|
+
occurredAt: '2026-09-05T09:10:00+08:00', state: 'active',
|
|
1865
|
+
invalidatedElementIds: ['Task_Review'], invalidatedEdgeIds: ['Flow_Submit_Review'],
|
|
1866
|
+
}],
|
|
1867
|
+
edgeVisits: beforeReview.map((visit) => visit.id === 'ev-2' ? { ...visit, status: 'superseded' } : visit),
|
|
1868
|
+
}
|
|
1869
|
+
const resubmitted: HostActivity = {
|
|
1870
|
+
...submitAgain, revision: 2, state: 'completed', endedAt: '2026-09-05T09:15:00+08:00',
|
|
1871
|
+
}
|
|
1872
|
+
const reviewAgain: HostActivity = {
|
|
1873
|
+
...review, key: { kind: 'task', id: 't-review-round-2' }, revision: 1,
|
|
1874
|
+
visitId: 'visit-review-2', startedAt: '2026-09-05T09:15:01+08:00',
|
|
1875
|
+
}
|
|
1876
|
+
export const reentryInput: HostTraceSource = {
|
|
1877
|
+
...rejectInput,
|
|
1878
|
+
historicalActivities: [start, submit, reviewed, resubmitted], currentActivities: [reviewAgain],
|
|
1879
|
+
operations: [submitAction, rejectAction, {
|
|
1880
|
+
...submitAction, id: 'op-submit-2', workItem: submitAgain.key, visitId: submitAgain.visitId,
|
|
1881
|
+
occurredAt: '2026-09-05T09:15:00+08:00', content: { plainText: '已补充材料,重新提交。' },
|
|
1882
|
+
}],
|
|
1883
|
+
movements: rejectInput.movements.map((movement) => ({
|
|
1884
|
+
...movement, state: 'resolved', resolvedAt: '2026-09-05T09:15:00+08:00', resolvedByActivityId: 'task:t-submit-2',
|
|
1885
|
+
})),
|
|
1886
|
+
edgeVisits: [...rejectInput.edgeVisits, {
|
|
1887
|
+
id: 'ev-3', edgeId: 'Flow_Submit_Review', occurredAt: '2026-09-05T09:15:01+08:00', status: 'effective',
|
|
1888
|
+
}],
|
|
1889
|
+
}
|
|
1890
|
+
|
|
1891
|
+
export const scenarioSnapshots = {
|
|
1892
|
+
single: { input: singleInput, runtime: toNovaRuntime(singleInput, ids) },
|
|
1893
|
+
waiting: { input: waitingInput, runtime: toNovaRuntime(waitingInput, ids) },
|
|
1894
|
+
completed: { input: completedInput, runtime: toNovaRuntime(completedInput, ids) },
|
|
1895
|
+
countersign: { input: countersignInput, runtime: toNovaRuntime(countersignInput, ids) },
|
|
1896
|
+
anySign: { input: anySignInput, runtime: toNovaRuntime(anySignInput, ids) },
|
|
1897
|
+
reject: { input: rejectInput, runtime: toNovaRuntime(rejectInput, ids) },
|
|
1898
|
+
reentry: { input: reentryInput, runtime: toNovaRuntime(reentryInput, ids) },
|
|
1899
|
+
}
|
|
1900
|
+
```
|
|
1901
|
+
|
|
1902
|
+
| 输入 → 完整输出 | 预期标准 Presentation 与默认展示 |
|
|
1903
|
+
| --- | --- |
|
|
1904
|
+
| `singleInput` → `scenarioSnapshots.single.runtime` | 3 条活动,而非 4 条;审核只有一个工作项,显示“处理中”、李经理 |
|
|
1905
|
+
| `waitingInput` → `scenarioSnapshots.waiting.runtime` | 审核 active,无处理人;本示例无候选配置,显示“待分配审批人” |
|
|
1906
|
+
| `completedInput` → `scenarioSnapshots.completed.runtime` | 流程 completed,审核已完成且有“通过”动作;结束边已经过 |
|
|
1907
|
+
| `countersignInput` → `scenarioSnapshots.countersign.runtime` | 审核同一 visit 中 3 个工作项,1 completed、2 active,显示“会签 1/3” |
|
|
1908
|
+
| `anySignInput` → `scenarioSnapshots.anySign.runtime` | 1 completed、2 cancelled,显示“或签 1/3”,门槛为 1,流程已完成;不为被取消工作项制造通过动作 |
|
|
1909
|
+
| `rejectInput` → `scenarioSnapshots.reject.runtime` | 提交节点第 2 次到达并 active,显示“重新审批”;审核保留驳回事实,旧审核路径失效,回退关系可见 |
|
|
1910
|
+
| `reentryInput` → `scenarioSnapshots.reentry.runtime` | 审核第 2 次到达显示处理中;旧驳回仍在历史内但已 resolved,回退线隐藏,新的 `ev-3` 恢复有效线路 |
|
|
1911
|
+
|
|
1912
|
+
`approval` 和 `compact` 会重排、聚合或过滤图元,展示不保证与 `standard` 的位置相同。普通开始/结束事件默认在完整 BPMN 可见,在实际路径/时间线默认隐藏。需要显示时设置 `runtimeTraceOptions.showStartMilestone / showEndMilestone`。
|
|
1913
|
+
|
|
1914
|
+
默认投影可能追加唯一可预测的后续人工节点;预测不是已发生事实,也不应写回 `activities` 或审批日志。需要严格只读历史时配置 `runtimeTraceOptions.includePredicted: false`;Studio 中放在 `config.viewer.runtimeTraceOptions`。
|
|
1915
|
+
|
|
1916
|
+
## 5. 加载、更新和交互
|
|
1917
|
+
|
|
1918
|
+
### 5.1 宿主加载契约
|
|
1919
|
+
|
|
1920
|
+
**自 `0.3.5-preview` 起的侧栏行为**:Studio `instance` 默认隐藏左右区域,右侧没有展开按钮,也不占宽度。下方示例仍显式写 `regions: { left: 'hidden', right: 'hidden' }`,可兼容旧版;如果要显示宿主业务详情,改为 `config.ui.regions.right: 'default'` 并使用 Vue `#right`、React `right` 或 Core `slots.right`。右侧 Slot 不会自动覆盖 `hidden`。
|
|
1921
|
+
|
|
1922
|
+
框架 Header/Right Context 的 `panelSelection` 和 Core `getPanelSelection()` / `subscribePanelSelection()` 提供当前选择、元素及轨迹上下文;Instance 点击不改变设计选择。面板可用默认 Flex 固定头尾/中间滚动,也可设为 Scroll 整体滚动。隐藏或收起右侧不关闭 `trace-click`、内置详情或附件交互,详见[侧栏指南](CUSTOMIZATION.md#侧栏布局与平滑折叠)。独立 `BpmnViewer` 不拥有 Shell 侧栏,不传入 `config.ui`。
|
|
1923
|
+
|
|
1924
|
+
下面的 `trace-contract.ts` 只定义宿主函数的返回约定,不创建 Nova HTTP 端点。宿主实现 `loadTrace` 时应完成读取、原始 JSON 校验、`toNovaRuntime()` 转换及完整性说明;返回的 XML 和 Runtime 必须属于同一实例的定义版本。历史不可用时由宿主报告错误或明确的 partial 状态,不能把查询失败转成“没有经过任何节点”。
|
|
1925
|
+
|
|
1926
|
+
```ts title="trace-contract.ts"
|
|
1927
|
+
import type { ProcessInstanceSnapshot } from '@bpmn-nova/vue'
|
|
1928
|
+
import type { HostTraceSource } from './runtime-adapter'
|
|
1929
|
+
|
|
1930
|
+
export interface TraceDocument {
|
|
1931
|
+
processDefinitionId: string
|
|
1932
|
+
xml: string
|
|
1933
|
+
runtime: ProcessInstanceSnapshot
|
|
1934
|
+
coverage: HostTraceSource['coverage']
|
|
1935
|
+
}
|
|
1936
|
+
export type LoadTrace = (instanceId: string, signal: AbortSignal) => Promise<TraceDocument>
|
|
1937
|
+
|
|
1938
|
+
export function checkTraceDocument(next: TraceDocument, instanceId: string, previous: TraceDocument | null): void {
|
|
1939
|
+
if (next.runtime.processInstanceId !== instanceId) throw new Error('审批轨迹响应的实例 ID 不匹配')
|
|
1940
|
+
if (!next.processDefinitionId || !next.xml.trim()) throw new Error('审批轨迹缺少定义版本或 XML')
|
|
1941
|
+
if (previous?.processDefinitionId === next.processDefinitionId && previous.xml !== next.xml) {
|
|
1942
|
+
throw new Error('同一定义版本返回了不同 XML,请检查定义服务')
|
|
1943
|
+
}
|
|
1944
|
+
}
|
|
1945
|
+
```
|
|
1946
|
+
|
|
1947
|
+
下面的刷新都是替换 Snapshot 对象,不是往原数组里 `push()`。Vue watcher 和 React effect 依赖 Prop 引用;闭包数据的定义态 `refreshPresentation()` 不能替代 Runtime 数据更新。
|
|
1948
|
+
|
|
1949
|
+
### 5.2 Vue:Studio 实例模式,防止旧请求覆盖
|
|
1950
|
+
|
|
1951
|
+
`ApprovalTrace.vue` 接收宿主的 `instanceId` 和 `loadTrace`,通过 `refresh()` 暴露主动刷新入口。首次加载显示占位,同实例刷新保留当前组件;切换实例先移除旧实例内容,避免旧数据冒充新实例。宿主函数变化时保持同样的取消语义。
|
|
1952
|
+
|
|
1953
|
+
```vue title="ApprovalTrace.vue"
|
|
1954
|
+
<script setup lang="ts">
|
|
1955
|
+
import { ref, shallowRef, watch } from 'vue'
|
|
1956
|
+
import { BpmnStudio } from '@bpmn-nova/vue'
|
|
1957
|
+
import type { BpmnStudioConfig, RuntimeTraceClickEvent } from '@bpmn-nova/vue'
|
|
1958
|
+
import '@bpmn-nova/vue/styles.css'
|
|
1959
|
+
import { checkTraceDocument } from './trace-contract'
|
|
1960
|
+
import type { LoadTrace, TraceDocument } from './trace-contract'
|
|
1961
|
+
|
|
1962
|
+
const props = defineProps<{ instanceId: string | null; loadTrace: LoadTrace }>()
|
|
1963
|
+
const emit = defineEmits<{ 'trace-click': [event: RuntimeTraceClickEvent] }>()
|
|
1964
|
+
const document = shallowRef<TraceDocument | null>(null)
|
|
1965
|
+
const loading = ref(false)
|
|
1966
|
+
const error = ref('')
|
|
1967
|
+
const refreshKey = ref(0)
|
|
1968
|
+
const config: BpmnStudioConfig = {
|
|
1969
|
+
ui: { regions: { left: 'hidden', right: 'hidden' } },
|
|
1970
|
+
viewer: { runtimeDetails: { autoOpen: false }, runtimeTraceOptions: { includePredicted: false } },
|
|
1971
|
+
}
|
|
1972
|
+
|
|
1973
|
+
watch(() => [props.instanceId, props.loadTrace, refreshKey.value] as const, async ([id, load], _previous, onCleanup) => {
|
|
1974
|
+
const controller = new AbortController()
|
|
1975
|
+
onCleanup(() => controller.abort())
|
|
1976
|
+
if (document.value?.runtime.processInstanceId !== id) document.value = null
|
|
1977
|
+
error.value = ''
|
|
1978
|
+
loading.value = Boolean(id)
|
|
1979
|
+
if (!id) return
|
|
1980
|
+
try {
|
|
1981
|
+
const next = await load(id, controller.signal)
|
|
1982
|
+
if (controller.signal.aborted) return
|
|
1983
|
+
checkTraceDocument(next, id, document.value)
|
|
1984
|
+
document.value = next
|
|
1985
|
+
} catch (cause) {
|
|
1986
|
+
if (!controller.signal.aborted) error.value = cause instanceof Error ? cause.message : '轨迹加载失败'
|
|
1987
|
+
} finally {
|
|
1988
|
+
if (!controller.signal.aborted) loading.value = false
|
|
1989
|
+
}
|
|
1990
|
+
}, { immediate: true })
|
|
1991
|
+
|
|
1992
|
+
defineExpose({ refresh: () => { refreshKey.value += 1 } })
|
|
1993
|
+
</script>
|
|
1994
|
+
|
|
1995
|
+
<template>
|
|
1996
|
+
<section>
|
|
1997
|
+
<p v-if="!instanceId">流程尚未发起</p>
|
|
1998
|
+
<p v-else-if="loading" role="status">{{ document ? '正在刷新轨迹…' : '正在加载轨迹…' }}</p>
|
|
1999
|
+
<p v-if="error" role="alert">{{ error }}</p>
|
|
2000
|
+
<p v-if="document && Object.values(document.coverage).includes('partial')">轨迹记录不完整,请结合业务审计查看。</p>
|
|
2001
|
+
<div v-if="document" style="height: 640px; min-height: 320px">
|
|
2002
|
+
<BpmnStudio :xml="document.xml" :runtime="document.runtime" :config="config"
|
|
2003
|
+
engine="flowable" mode="instance" :allowed-modes="['instance']" projection="approval"
|
|
2004
|
+
@trace-click="emit('trace-click', $event)" />
|
|
2005
|
+
</div>
|
|
2006
|
+
</section>
|
|
2007
|
+
</template>
|
|
2008
|
+
```
|
|
2009
|
+
|
|
2010
|
+
这里禁用了内置详情自动打开,宿主监听 `trace-click` 自行展示。需要 Nova 默认详情时设 `config.viewer.runtimeDetails.autoOpen: true`,宿主事件只做观察。卸载时 Vue 自动清理 watcher 并取消当前请求,适配组件负责销毁自己创建的实例。
|
|
2011
|
+
|
|
2012
|
+
### 5.3 React:独立 Viewer 与 Studio 选择
|
|
2013
|
+
|
|
2014
|
+
`ApprovalTrace.tsx` 使用相同的宿主加载契约。`loadTrace` 应由宿主以稳定函数引用传入;`refreshKey` 变化代表一次新刷新,不应作为组件 React `key` 使用。
|
|
2015
|
+
|
|
2016
|
+
```tsx title="ApprovalTrace.tsx"
|
|
2017
|
+
import { useEffect, useRef, useState } from 'react'
|
|
2018
|
+
import { BpmnStudio, BpmnViewer } from '@bpmn-nova/react'
|
|
2019
|
+
import type { BpmnStudioConfig, RuntimeTraceClickEvent } from '@bpmn-nova/react'
|
|
2020
|
+
import '@bpmn-nova/react/styles.css'
|
|
2021
|
+
import { checkTraceDocument } from './trace-contract'
|
|
2022
|
+
import type { LoadTrace, TraceDocument } from './trace-contract'
|
|
2023
|
+
|
|
2024
|
+
const studioConfig: BpmnStudioConfig = {
|
|
2025
|
+
ui: { regions: { left: 'hidden', right: 'hidden' } },
|
|
2026
|
+
viewer: { runtimeDetails: { autoOpen: false }, runtimeTraceOptions: { includePredicted: false } },
|
|
2027
|
+
}
|
|
2028
|
+
const details = { autoOpen: false }
|
|
2029
|
+
const traceOptions = { includePredicted: false }
|
|
2030
|
+
|
|
2031
|
+
export function ApprovalTrace({ instanceId, loadTrace, refreshKey = 0, useStudio = false, onTraceClick }: {
|
|
2032
|
+
instanceId: string | null
|
|
2033
|
+
loadTrace: LoadTrace
|
|
2034
|
+
refreshKey?: number
|
|
2035
|
+
useStudio?: boolean
|
|
2036
|
+
onTraceClick?: (event: RuntimeTraceClickEvent) => void
|
|
2037
|
+
}) {
|
|
2038
|
+
const [document, setDocument] = useState<TraceDocument | null>(null)
|
|
2039
|
+
const lastAccepted = useRef<TraceDocument | null>(null)
|
|
2040
|
+
const [loading, setLoading] = useState(false)
|
|
2041
|
+
const [error, setError] = useState('')
|
|
2042
|
+
useEffect(() => {
|
|
2043
|
+
const controller = new AbortController()
|
|
2044
|
+
if (lastAccepted.current?.runtime.processInstanceId !== instanceId) lastAccepted.current = null
|
|
2045
|
+
setError('')
|
|
2046
|
+
setLoading(Boolean(instanceId))
|
|
2047
|
+
if (!instanceId) { setDocument(null); return () => controller.abort() }
|
|
2048
|
+
void (async () => {
|
|
2049
|
+
try {
|
|
2050
|
+
const next = await loadTrace(instanceId, controller.signal)
|
|
2051
|
+
if (controller.signal.aborted) return
|
|
2052
|
+
checkTraceDocument(next, instanceId, lastAccepted.current)
|
|
2053
|
+
lastAccepted.current = next
|
|
2054
|
+
setDocument(next)
|
|
2055
|
+
} catch (cause) {
|
|
2056
|
+
if (!controller.signal.aborted) setError(cause instanceof Error ? cause.message : '轨迹加载失败')
|
|
2057
|
+
} finally {
|
|
2058
|
+
if (!controller.signal.aborted) setLoading(false)
|
|
2059
|
+
}
|
|
2060
|
+
})()
|
|
2061
|
+
return () => controller.abort()
|
|
2062
|
+
}, [instanceId, loadTrace, refreshKey])
|
|
2063
|
+
// render 时即按实例身份过滤,避免新请求的 effect 执行前短暂显示旧实例。
|
|
2064
|
+
const visible = document?.runtime.processInstanceId === instanceId ? document : null
|
|
2065
|
+
return <section>
|
|
2066
|
+
{!instanceId && <p>流程尚未发起</p>}
|
|
2067
|
+
{loading && <p role="status">{visible ? '正在刷新轨迹…' : '正在加载轨迹…'}</p>}
|
|
2068
|
+
{error && <p role="alert">{error}</p>}
|
|
2069
|
+
{visible && Object.values(visible.coverage).includes('partial') && <p>轨迹记录不完整,请结合业务审计查看。</p>}
|
|
2070
|
+
{visible && <div style={{ height: 640, minHeight: 320 }}>
|
|
2071
|
+
{useStudio
|
|
2072
|
+
? <BpmnStudio xml={visible.xml} runtime={visible.runtime} config={studioConfig}
|
|
2073
|
+
engine="flowable" mode="instance" allowedModes={['instance']} projection="approval" onTraceClick={onTraceClick} />
|
|
2074
|
+
: <BpmnViewer xml={visible.xml} runtime={visible.runtime} engine="flowable" projection="approval"
|
|
2075
|
+
runtimeDetails={details} runtimeTraceOptions={traceOptions} onTraceClick={onTraceClick} />}
|
|
2076
|
+
</div>}
|
|
2077
|
+
</section>
|
|
2078
|
+
}
|
|
2079
|
+
```
|
|
2080
|
+
|
|
2081
|
+
`useStudio` 是本例宿主布局选择,不是 Nova Prop;运行中切换它会切换组件种类。普通刷新保持它不变,使用新 Runtime 对象更新既有组件。Vue 独立 Viewer 使用相同顶层 `runtime-details` / `runtime-trace-options` 配置;只有完整 Studio 使用 `config.viewer`。
|
|
2082
|
+
|
|
2083
|
+
### 5.4 Core:显式生命周期与更新
|
|
2084
|
+
|
|
2085
|
+
`core-trace.ts` 的两个工厂分别提供独立 Viewer 和完整 Studio。只在浏览器中调用,传入有高度的容器;调用方从宿主加载函数得到成功且未过期的 `TraceDocument` 后,再调用 `update()`。同实例、同定义的刷新只设置 Runtime;换实例或定义时才替换模型。
|
|
2086
|
+
|
|
2087
|
+
```ts title="core-trace.ts"
|
|
2088
|
+
import { BpmnViewer, createStudioController, createStudioShell, importBpmn } from '@bpmn-nova/studio'
|
|
2089
|
+
import type { RuntimeTraceClickEvent } from '@bpmn-nova/studio'
|
|
2090
|
+
import '@bpmn-nova/studio/styles.css'
|
|
2091
|
+
import { checkTraceDocument } from './trace-contract'
|
|
2092
|
+
import type { TraceDocument } from './trace-contract'
|
|
2093
|
+
|
|
2094
|
+
export function mountViewer(container: HTMLElement, initial: TraceDocument, onTraceClick: (event: RuntimeTraceClickEvent) => void) {
|
|
2095
|
+
let current = initial
|
|
2096
|
+
const viewer = new BpmnViewer({
|
|
2097
|
+
container, model: importBpmn(initial.xml, 'flowable'), runtime: initial.runtime,
|
|
2098
|
+
projection: 'approval', runtimeDetails: { autoOpen: false },
|
|
2099
|
+
runtimeTraceOptions: { includePredicted: false }, onTraceClick,
|
|
2100
|
+
})
|
|
2101
|
+
return {
|
|
2102
|
+
update(next: TraceDocument) {
|
|
2103
|
+
checkTraceDocument(next, next.runtime.processInstanceId, current)
|
|
2104
|
+
if (next.runtime.processInstanceId !== current.runtime.processInstanceId || next.processDefinitionId !== current.processDefinitionId) {
|
|
2105
|
+
viewer.setModel(importBpmn(next.xml, 'flowable'))
|
|
2106
|
+
}
|
|
2107
|
+
viewer.setRuntime(next.runtime)
|
|
2108
|
+
current = next
|
|
2109
|
+
},
|
|
2110
|
+
destroy: () => viewer.destroy(),
|
|
2111
|
+
}
|
|
2112
|
+
}
|
|
2113
|
+
|
|
2114
|
+
export function mountStudio(container: HTMLElement, initial: TraceDocument, onTraceClick: (event: RuntimeTraceClickEvent) => void) {
|
|
2115
|
+
let current = initial
|
|
2116
|
+
const studio = createStudioController({ model: importBpmn(initial.xml, 'flowable') })
|
|
2117
|
+
const shell = createStudioShell({
|
|
2118
|
+
container, studio, runtime: initial.runtime, mode: 'instance', allowedModes: ['instance'], projection: 'approval',
|
|
2119
|
+
config: {
|
|
2120
|
+
ui: { regions: { left: 'hidden', right: 'hidden' } },
|
|
2121
|
+
viewer: { runtimeDetails: { autoOpen: false }, runtimeTraceOptions: { includePredicted: false } },
|
|
2122
|
+
},
|
|
2123
|
+
rendererOptions: { onTraceClick },
|
|
2124
|
+
})
|
|
2125
|
+
return {
|
|
2126
|
+
update(next: TraceDocument) {
|
|
2127
|
+
checkTraceDocument(next, next.runtime.processInstanceId, current)
|
|
2128
|
+
if (next.runtime.processInstanceId !== current.runtime.processInstanceId || next.processDefinitionId !== current.processDefinitionId) {
|
|
2129
|
+
studio.setModel(importBpmn(next.xml, 'flowable'))
|
|
2130
|
+
}
|
|
2131
|
+
shell.setRuntime(next.runtime)
|
|
2132
|
+
current = next
|
|
2133
|
+
},
|
|
2134
|
+
destroy() { shell.destroy(); studio.destroy() },
|
|
2135
|
+
}
|
|
2136
|
+
}
|
|
2137
|
+
```
|
|
2138
|
+
|
|
2139
|
+
Core 的调用方仍须保存当前请求的实例 ID,并使用 `AbortController` 和过期响应检查,不能直接把任意请求结果传给 `update()`。切换实例和定义是有意的模型替换,不属于“同实例刷新不重导 XML”的保证。不要在每次刷新后自动 Fit,否则会覆盖用户正在阅读的位置。
|
|
2140
|
+
|
|
2141
|
+
### 5.5 点击数据与内置详情
|
|
2142
|
+
|
|
2143
|
+
`RuntimeTraceClickEvent.targetType` 区分 `node / edge / visit / transition`,并提供可选的原始 `elementId`、`visitId`、`traceItem`、`transition`、`presentation`,以及 `activityInstances`、`actions`、`runtime` 和实际 `projection`。
|
|
2144
|
+
|
|
2145
|
+
所有分支应先判断 `targetType` 和可选字段,不能把每次点击都当成人工任务。在审批投影中使用事件的原始元素/访问关联,不从重排后的 DOM 属性反推业务任务 ID。事件是展示交互,不代表已经执行通过、驳回或保存;执行这些操作仍需要宿主校验权限并调用自己的业务 API。
|
|
2146
|
+
|
|
2147
|
+
## 6. 意见、图片与附件
|
|
2148
|
+
|
|
2149
|
+
以下 `approval-assets.ts` 为一次真实已记录的操作补充内容,并提供可注入 Viewer/Studio 的 Resolver 工厂。资源名称、MIME、大小来自宿主元数据;审批快照不存短期地址,用户头像也不要填入凭证或短期签名。
|
|
2150
|
+
|
|
2151
|
+
```ts title="approval-assets.ts"
|
|
2152
|
+
import type { RuntimeApprovalContent, RuntimeAssetPurpose, RuntimeAssetResolver } from '@bpmn-nova/vue'
|
|
2153
|
+
import { completedInput } from './runtime-scenarios'
|
|
2154
|
+
import type { HostTraceSource } from './runtime-adapter'
|
|
2155
|
+
|
|
2156
|
+
export const content: RuntimeApprovalContent = {
|
|
2157
|
+
plainText: '同意,核验材料见附件。',
|
|
2158
|
+
blocks: [
|
|
2159
|
+
{ type: 'paragraph', text: '同意,核验材料见附件。' },
|
|
2160
|
+
{ type: 'image', assetId: 'asset-photo', alt: '材料照片' },
|
|
2161
|
+
{ type: 'file', assetId: 'asset-checklist' },
|
|
2162
|
+
],
|
|
2163
|
+
assets: [
|
|
2164
|
+
{ id: 'asset-photo', name: '材料照片.png', mediaType: 'image/png', width: 1200, height: 800 },
|
|
2165
|
+
{ id: 'asset-checklist', name: '核验清单.pdf', mediaType: 'application/pdf', size: 4096 },
|
|
2166
|
+
],
|
|
2167
|
+
}
|
|
2168
|
+
export const withAssetsInput: HostTraceSource = {
|
|
2169
|
+
...completedInput,
|
|
2170
|
+
operations: completedInput.operations.map((operation) => operation.id === 'op-approve-1' ? { ...operation, content } : operation),
|
|
2171
|
+
}
|
|
2172
|
+
|
|
2173
|
+
type ResolveAssetUrl = (input: {
|
|
2174
|
+
assetId: string; actionId: string; purpose: RuntimeAssetPurpose; signal: AbortSignal
|
|
2175
|
+
}) => Promise<string | null>
|
|
2176
|
+
|
|
2177
|
+
export function createAssetResolver(resolveAssetUrl: ResolveAssetUrl): RuntimeAssetResolver {
|
|
2178
|
+
return (asset, { action, purpose, signal }) => resolveAssetUrl({
|
|
2179
|
+
assetId: asset.id, actionId: action.id, purpose, signal,
|
|
2180
|
+
})
|
|
2181
|
+
}
|
|
2182
|
+
```
|
|
2183
|
+
|
|
2184
|
+
将 `withAssetsInput` 交给同一个 `toNovaRuntime()`,输出保留一个通过动作,内容包含一张图片和一个文件;不能为“补全附件展示”再新增一个重复审批操作。`resolveAssetUrl` 是宿主函数,按当前用户、实例/操作、资源及用途进行授权后返回资源地址,不返回业务 JSON 接口本身的地址。
|
|
2185
|
+
|
|
2186
|
+
`purpose` 为 `thumbnail / preview / download / export`。宿主应把 `signal` 传递到实际网络请求并及时取消;返回相对或绝对 HTTP(S) 地址、宿主管理生命周期的 Blob URL,或者无权限/不存在时返回 `null`。资源不可用时 Nova 局部降级,不应让整条审批轨迹失败。需特别检查跨域图片、实际响应 MIME 与下载权限。
|
|
2187
|
+
|
|
2188
|
+
原生内容块解释 `paragraph / image / file`,不执行 HTML 或 Markdown。默认图片预览和附件下载由 Viewer 处理;SVG 导出会按 `export` 用途获取图片并嵌入 Artifact,普通文件只保留元数据,不把临时地址写回 Snapshot。上传、存储、删除、审计和签名生命周期属于宿主业务。
|
|
2189
|
+
|
|
2190
|
+
## 7. 接入验收与排错
|
|
2191
|
+
|
|
2192
|
+
| 现象 | 优先检查与处理 |
|
|
2193
|
+
| --- | --- |
|
|
2194
|
+
| 画布空白 | 样式入口、浏览器生命周期、容器与父级实际高度;先区分布局问题与数据加载失败 |
|
|
2195
|
+
| 无实例 ID | 宿主展示“尚未发起”,等待真实实例;不创建虚假的 running Snapshot |
|
|
2196
|
+
| 请求中或请求失败 | 首次加载显示占位;同实例刷新失败保留上次成功数据并提示错误 |
|
|
2197
|
+
| 历史为空 | 检查历史级别、保留策略、分页及权限;与真正尚未到达的节点区分 |
|
|
2198
|
+
| 处理人或状态消失 | 检查定义版本、原始节点 ID、人员名称,以及实时/历史记录是否重复或相互覆盖 |
|
|
2199
|
+
| 会签人数不对 | 检查同轮 visitId、独立工作项 ID、显式 approvalMode、人数与门槛;同一工作项不能在两个来源各计一次 |
|
|
2200
|
+
| 驳回没有线路 | 检查 Transition、actionId、来源/目标、时间和失效范围;Action 本身不是线路 |
|
|
2201
|
+
| 重新审批仍显示旧状态 | 新到达是否获得新 visitId 与工作项 ID;新 edgeVisit 是否有更晚时间;回退是否正确 resolved |
|
|
2202
|
+
| 历史操作重复 | 检查显式 Action 的 activityId 和 Transition actionId,避免旧 comment/outcome 再次生成兼容动作 |
|
|
2203
|
+
| 出现未处理的后续节点 | 检查是否预测节点;严格历史视图设置 includePredicted: false |
|
|
2204
|
+
| 修改数组后没有刷新 | 替换 runtime 对象引用;Core 调用 setRuntime,不依赖定义态 refreshPresentation |
|
|
2205
|
+
| 意见附件不可用 | 核对 assetId、Resolver、用途授权、取消信号、跨域与真实资源响应 |
|
|
2206
|
+
|
|
2207
|
+
完成接入应逐项确认:
|
|
2208
|
+
|
|
2209
|
+
1. 安装入口与框架一致,公共类型及生产构建通过;`0.3.5-preview` 之前缺少具名 Runtime 类型出口的版本使用兼容推导。
|
|
2210
|
+
2. 真实 XML 的节点/连线 ID 与记录关联一致,刷新前后定义 XML 不变。
|
|
2211
|
+
3. 单人、等待、会签、或签、驳回及重入均符合宿主业务记录;结束任务没有被自动解释为通过。
|
|
2212
|
+
4. 同实例刷新不重建组件或重复导入 XML;切换实例取消旧请求,响应身份不匹配时拒绝覆盖。
|
|
2213
|
+
5. `standard / approval / compact` 使用相同事实;预测与真实记录可区分,历史失效记录仍可追溯。
|
|
2214
|
+
6. 内置详情与宿主详情按 autoOpen 策略工作,无重复事件订阅;Light/Dark 和窄屏下内容可读。
|
|
2215
|
+
7. 附件授权失败仅局部降级,卸载后请求与实例得到清理。
|
|
2216
|
+
|
|
2217
|
+
这些是宿主集成验收项,不代表仅复制本文就已完成真实引擎联调。示例不替宿主创建业务接口、部署流程或运行审批事务。完整参数见[组件参数](COMPONENTS.md)、[公开 API](API.md)与[自定义指南](CUSTOMIZATION.md);安装入口见 [AI 接入指南](AI-INTEGRATION.md)。
|
|
2218
|
+
|
|
2219
|
+
|
|
1243
2220
|
<!-- SOURCE: docs/GETTING-STARTED.md -->
|
|
1244
2221
|
|
|
1245
2222
|
# BPMN Nova 快速开始
|
|
1246
2223
|
|
|
1247
|
-
本文面向通过 NPM 集成 BPMN Nova 的应用开发者。版本 `0.3.
|
|
2224
|
+
本文面向通过 NPM 集成 BPMN Nova 的应用开发者。版本 `0.3.5-preview` 要求现代浏览器;Node.js 18+ 用于构建、SSR 和开发工具。
|
|
1248
2225
|
|
|
1249
2226
|
## 1. 按语言与框架选择入口
|
|
1250
2227
|
|
|
@@ -1316,16 +2293,24 @@ const model = createEmptyProcess('flowable')
|
|
|
1316
2293
|
model.id = 'Process_LeaveApproval'
|
|
1317
2294
|
model.name = '请假审批'
|
|
1318
2295
|
|
|
2296
|
+
const config = {
|
|
2297
|
+
modeling: {
|
|
2298
|
+
propertiesProfile: 'business',
|
|
2299
|
+
allowedNodeTypes: ['startEvent', 'userTask', 'exclusiveGateway', 'endEvent'],
|
|
2300
|
+
allowedEdgeTypes: ['sequenceFlow'],
|
|
2301
|
+
},
|
|
2302
|
+
ui: { controlSize: 'medium' },
|
|
2303
|
+
}
|
|
2304
|
+
|
|
1319
2305
|
const studio = createStudioController({
|
|
1320
2306
|
model,
|
|
1321
|
-
|
|
1322
|
-
allowedNodeTypes: ['startEvent', 'userTask', 'exclusiveGateway', 'endEvent'],
|
|
1323
|
-
allowedEdgeTypes: ['sequenceFlow'],
|
|
2307
|
+
config,
|
|
1324
2308
|
})
|
|
1325
2309
|
|
|
1326
2310
|
const shell = createStudioShell({
|
|
1327
2311
|
container: document.querySelector('#studio'),
|
|
1328
2312
|
studio,
|
|
2313
|
+
config,
|
|
1329
2314
|
mode: 'design',
|
|
1330
2315
|
allowedModes: ['design', 'viewer'],
|
|
1331
2316
|
theme: 'auto',
|
|
@@ -1348,6 +2333,8 @@ function dispose() {
|
|
|
1348
2333
|
|
|
1349
2334
|
Controller 持有模型、命令、选择、历史与作用域;Shell 只负责组合 Canvas、Palette、Properties 和 Viewer。销毁 Shell 不会自动销毁外部传入的 Controller。
|
|
1350
2335
|
|
|
2336
|
+
`config` 用一个结构化入口承载静态选项:`modeling` 传给 Controller,`ui`、`viewer` 和 `export` 传给 Shell。旧顶层字段仍兼容但已 Deprecated;同一项同时配置时,显式旧值优先。
|
|
2337
|
+
|
|
1351
2338
|
宿主可观察实际 Mode,并在运行时调整白名单:
|
|
1352
2339
|
|
|
1353
2340
|
```js
|
|
@@ -1431,6 +2418,8 @@ Designer 子路径适合已有自定义工具栏和属性面板的宿主。需
|
|
|
1431
2418
|
|
|
1432
2419
|
## 6. Studio Viewer 子路径与审批轨迹
|
|
1433
2420
|
|
|
2421
|
+
接入真实审批后端时先阅读[审批轨迹接入指南](RUNTIME-INTEGRATION.md),按实例定义版本准备 XML,再转换工作项、操作及路径事实。指南提供完整可编译示例,以及新增 Runtime 类型出口在 `0.3.5-preview` 之前版本的兼容写法;设置 `engine` 不会自动查询引擎。
|
|
2422
|
+
|
|
1434
2423
|
```js
|
|
1435
2424
|
import { BpmnViewer } from '@bpmn-nova/studio/viewer'
|
|
1436
2425
|
import '@bpmn-nova/studio/styles.css'
|
|
@@ -1496,8 +2485,11 @@ export function WorkflowEditor({ initialXml }) {
|
|
|
1496
2485
|
engine="flowable"
|
|
1497
2486
|
mode={mode}
|
|
1498
2487
|
allowedModes={['design', 'viewer']}
|
|
2488
|
+
config={{
|
|
2489
|
+
modeling: { propertiesProfile: 'business' },
|
|
2490
|
+
ui: { controlSize: 'medium' },
|
|
2491
|
+
}}
|
|
1499
2492
|
nodeSubtitleResolver={subtitleResolver}
|
|
1500
|
-
propertiesProfile="business"
|
|
1501
2493
|
theme="auto"
|
|
1502
2494
|
onChange={(model, reason, nextXml) => saveDraft(nextXml)}
|
|
1503
2495
|
onSelectionChange={(selection, element) => console.log(element)}
|
|
@@ -1545,7 +2537,10 @@ const onChange = (model, reason, nextXml) => saveDraft(nextXml)
|
|
|
1545
2537
|
engine="flowable"
|
|
1546
2538
|
v-model:mode="mode"
|
|
1547
2539
|
:allowed-modes="['design', 'viewer']"
|
|
1548
|
-
|
|
2540
|
+
:config="{
|
|
2541
|
+
modeling: { propertiesProfile: 'business' },
|
|
2542
|
+
ui: { controlSize: 'medium' },
|
|
2543
|
+
}"
|
|
1549
2544
|
theme="auto"
|
|
1550
2545
|
@change="onChange"
|
|
1551
2546
|
@mode-change="event => console.log(event.source)"
|
|
@@ -1568,11 +2563,12 @@ Vue 事件使用 kebab-case:`change`、`selection-change`、`scope-change`、`
|
|
|
1568
2563
|
ref="studioRef"
|
|
1569
2564
|
:xml="xml"
|
|
1570
2565
|
engine="activiti"
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
2566
|
+
mode="design"
|
|
2567
|
+
:allowed-modes="['design']"
|
|
2568
|
+
:config="{
|
|
2569
|
+
modeling: { allowedNodeTypes: supportedNodeTypes, allowedEdgeTypes: ['sequenceFlow'] },
|
|
2570
|
+
ui: { controlSize: 'medium', regions: { right: 'hidden' } },
|
|
2571
|
+
}"
|
|
1576
2572
|
theme="auto"
|
|
1577
2573
|
@change="handleChange"
|
|
1578
2574
|
@selection-change="handleSelectionChange"
|
|
@@ -1581,7 +2577,7 @@ Vue 事件使用 kebab-case:`change`、`selection-change`、`scope-change`、`
|
|
|
1581
2577
|
<template #header-start="{ state, actions }">
|
|
1582
2578
|
<!-- 返回、业务图标、流程名称、类型 -->
|
|
1583
2579
|
</template>
|
|
1584
|
-
|
|
2580
|
+
<template #header-actions="{ actions, mode, ui }">
|
|
1585
2581
|
<button type="button" :disabled="mode !== 'design'" @click="actions.validate()">校验</button>
|
|
1586
2582
|
<button type="button" :disabled="mode !== 'design'" @click="saveDraft(actions.exportXml())">保存</button>
|
|
1587
2583
|
<button type="button" :disabled="mode !== 'design'" @click="publishProcess(actions)">发布</button>
|
|
@@ -1591,6 +2587,28 @@ Vue 事件使用 kebab-case:`change`、`selection-change`、`scope-change`、`
|
|
|
1591
2587
|
|
|
1592
2588
|
Vue 的 `#header-start` 只替换 Brand,`#header-actions` 只替换默认“校验 / 导入 / 导出”动作组,`#header` 替换完整 Header;React 使用等价的 `headerStart` / `headerActions` / `header` Render Prop。顶部最佳视图已去重,底部入口与 `fitView()` 保留。`actions.validate()` 先展示 Nova 内部结果,再发送 `validation` / `onValidation`,最后返回 issues。`regions` 属性变化会原地更新区域,不重建 Canvas。
|
|
1593
2589
|
|
|
2590
|
+
### 8.2 对齐宿主 UI 组件库
|
|
2591
|
+
|
|
2592
|
+
Header 控件通过 `config.ui.controlSize` 统一设置。默认值是 `medium`;`small / medium / large` 的普通按钮高度分别为 28 / 32 / 40px,也可传入 `24px`–`48px` 的 CSS 像素字符串:
|
|
2593
|
+
|
|
2594
|
+
```vue
|
|
2595
|
+
<BpmnStudio
|
|
2596
|
+
ref="studioRef"
|
|
2597
|
+
:config="{ ui: { controlSize: '36px' } }"
|
|
2598
|
+
>
|
|
2599
|
+
<template #header-actions="{ actions, ui }">
|
|
2600
|
+
<HostWorkflowActions
|
|
2601
|
+
:actions="actions"
|
|
2602
|
+
:button-height="ui.controlHeight"
|
|
2603
|
+
/>
|
|
2604
|
+
</template>
|
|
2605
|
+
</BpmnStudio>
|
|
2606
|
+
```
|
|
2607
|
+
|
|
2608
|
+
Element Plus 的 `small / default / large` 可映射到 Nova `small / medium / large`;Ant Design 的 `small / middle / large` 使用相同映射。自定义动作位于 Nova Header DOM 内,可继承 `--nova-control-height`、`--nova-header-height`、`--nova-control-font-size`、`--nova-control-padding-inline`、`--nova-control-icon-size` 和 `--nova-control-radius`。
|
|
2609
|
+
|
|
2610
|
+
该尺寸不会影响 Footer 缩放区、Canvas 浮动工具、Palette 或 Properties。运行时可通过 `shell.setConfig()` 或框架响应式 `config` 原地更新;`shell.getConfig()` 返回当前归一化配置。`config.modeling` 只在 Controller 创建时生效,运行中只允许现有 `setPropertiesProfile()` 调整 Profile,节点/连线白名单不会改变。
|
|
2611
|
+
|
|
1594
2612
|
```js
|
|
1595
2613
|
async function publishProcess(actions) {
|
|
1596
2614
|
const issues = actions.validate()
|
|
@@ -1599,10 +2617,25 @@ async function publishProcess(actions) {
|
|
|
1599
2617
|
}
|
|
1600
2618
|
```
|
|
1601
2619
|
|
|
1602
|
-
|
|
2620
|
+
宿主拥有外部属性面板时,以 `selection-change` / `onSelectionChange` 作为设计选择状态来源,用稳定 BPMN Element ID 关联业务配置;`element-click` 不能替代选择状态。希望复用 Nova 侧栏时使用下述右侧 Slot。Nova 不负责保存、发布、权限或服务端事务。
|
|
1603
2621
|
|
|
1604
2622
|
`theme="auto"` 需要显式配置才会跟随系统主题;未传 Theme 时仍默认为 `light`。完全自定义布局时,React 与 Vue 均可组合 `BpmnCanvas`、`BpmnPalettePanel` 和 `BpmnPropertiesPanel`,并传入同一个外部 Controller。
|
|
1605
2623
|
|
|
2624
|
+
### 8.3 右侧内容与侧栏展开收起
|
|
2625
|
+
|
|
2626
|
+
Vue `#right`、React `right`、Core `slots.right` 替换右侧内容但保留 Nova 管理的宽度、高度和折叠按钮。不要配置 `regions.right: 'hidden'` 后再期待 Slot 显示;隐藏移除整个区域,收起才保留展开按钮。
|
|
2627
|
+
|
|
2628
|
+
```js
|
|
2629
|
+
const config = { ui: {
|
|
2630
|
+
leftPanel: { collapsible: true, defaultCollapsed: false },
|
|
2631
|
+
rightPanel: { collapsible: true, defaultCollapsed: false, layout: 'flex' },
|
|
2632
|
+
} }
|
|
2633
|
+
```
|
|
2634
|
+
|
|
2635
|
+
默认 Flex 让宿主安排固定头尾与 `flex: 1; min-height: 0; overflow: auto` 中间内容;改为 `scroll` 时由 Nova 滚动整块内容。父容器仍需完整高度链。默认折叠按钮位于左右分隔线中点,200ms 平滑过渡并尊重减少动态效果;不卸载内容、不自动 Fit。Shell 容器 ≤720px 时首次收起并保留按钮,展开继续挤压画布,宽窄屏分别记忆状态。
|
|
2636
|
+
|
|
2637
|
+
Instance 默认隐藏右侧;显式 `regions.right: 'default'` 可开启,隐藏侧栏不影响轨迹点击/详情。通过 `panelSelection` 读取跨模式的当前元素,而非从只读页面猜测设计 `state.selection`。完整 Core/Vue/React 示例、状态方法和事件见[侧栏指南](CUSTOMIZATION.md#侧栏布局与平滑折叠);这些新增能力自 `0.3.5-preview` 提供。
|
|
2638
|
+
|
|
1606
2639
|
## 9. Group 与 Pool/Lane
|
|
1607
2640
|
|
|
1608
2641
|
Group 是纯视觉 Artifact,不保存成员关系:
|
|
@@ -1660,7 +2693,7 @@ BPMN Nova 主题应挂在组件根容器。自定义 Renderer 不要修改 `docu
|
|
|
1660
2693
|
|
|
1661
2694
|
# BPMN Nova 组件参数
|
|
1662
2695
|
|
|
1663
|
-
本手册记录 `0.3.
|
|
2696
|
+
本手册记录 `0.3.5-preview` 的公开组件参数、回调和实例方法。所有视觉组件都需要具有实际尺寸的容器,并显式导入所属包的 `styles.css`。
|
|
1664
2697
|
|
|
1665
2698
|
## 按项目环境阅读
|
|
1666
2699
|
|
|
@@ -1681,6 +2714,7 @@ type EngineId = 'flowable' | 'activiti'
|
|
|
1681
2714
|
type ViewerProjection = 'auto' | 'standard' | 'approval' | 'compact'
|
|
1682
2715
|
type NovaThemeInput = 'light' | 'dark' | 'auto' | NovaThemeOptions
|
|
1683
2716
|
type PropertiesProfile = 'business' | 'developer'
|
|
2717
|
+
type StudioControlSize = 'small' | 'medium' | 'large' | `${number}px`
|
|
1684
2718
|
```
|
|
1685
2719
|
|
|
1686
2720
|
完整 `ProcessModel`、`BpmnNode`、`BpmnEdge` 与 Runtime Snapshot 类型见对应包的 `.d.ts` 和 [API](API.md)。
|
|
@@ -1693,10 +2727,11 @@ Controller 不创建 DOM,负责模型、命令、历史、选择与子流程
|
|
|
1693
2727
|
| --- | --- | --- | --- |
|
|
1694
2728
|
| `model` | `ProcessModel` | 必填 | 初始流程模型 |
|
|
1695
2729
|
| `historyLimit` | `number` | `80` | Undo/Redo 快照上限 |
|
|
1696
|
-
| `
|
|
2730
|
+
| `config` | `BpmnStudioConfig` | `{}` | 结构化静态配置;Controller 使用 `config.modeling` |
|
|
2731
|
+
| `propertiesProfile` | `'business' \| 'developer'` | `'business'` | Deprecated 兼容别名;优先于 `config.modeling.propertiesProfile` |
|
|
1697
2732
|
| `extensions` | `StudioExtension[]` | `[]` | Controller 扩展,`setup()` 可返回清理函数 |
|
|
1698
|
-
| `allowedNodeTypes` | `Iterable<NodeType> \| null` | `null` |
|
|
1699
|
-
| `allowedEdgeTypes` | `Iterable<EdgeType> \| null` | `null` |
|
|
2733
|
+
| `allowedNodeTypes` | `Iterable<NodeType> \| null` | `null` | Deprecated 兼容别名;完整模型节点白名单 |
|
|
2734
|
+
| `allowedEdgeTypes` | `Iterable<EdgeType> \| null` | `null` | Deprecated 兼容别名;完整模型连线白名单 |
|
|
1700
2735
|
|
|
1701
2736
|
常用方法:
|
|
1702
2737
|
|
|
@@ -1719,17 +2754,18 @@ Controller 不创建 DOM,负责模型、命令、历史、选择与子流程
|
|
|
1719
2754
|
| --- | --- | --- | --- |
|
|
1720
2755
|
| `container` | `HTMLElement` | 必填 | Studio 根容器 |
|
|
1721
2756
|
| `studio` | `BpmnStudioController` | 必填 | 外部 Controller |
|
|
2757
|
+
| `config` | `BpmnStudioConfig` | `{}` | `ui/viewer/export` 静态配置;`modeling` 由 Controller 创建时消费 |
|
|
1722
2758
|
| `mode` | `'design' \| 'viewer' \| 'instance'` | `'design'` | 当前展示模式 |
|
|
1723
2759
|
| `allowedModes` | `('design' \| 'viewer' \| 'instance')[]` | 全部模式 | 默认工作台显示并允许切换的模式 |
|
|
1724
2760
|
| `runtime` | `ProcessInstanceSnapshot \| null` | `null` | 实例模式数据;没有数据时保持空运行事实 |
|
|
1725
2761
|
| `projection` | `ViewerProjection` | 实例模式默认 `approval` | 审批轨迹投影 |
|
|
1726
|
-
| `responsive` | `boolean` | `false` |
|
|
1727
|
-
| `projectionOptions` | `{ value, label }[]` | 默认两项 |
|
|
1728
|
-
| `leftWidth` | `number` | `244` |
|
|
1729
|
-
| `rightWidth` | `number` | `360` |
|
|
2762
|
+
| `responsive` | `boolean` | `false` | Deprecated;使用 `config.viewer.responsive` |
|
|
2763
|
+
| `projectionOptions` | `{ value, label }[]` | 默认两项 | Deprecated;使用 `config.viewer.projectionOptions` |
|
|
2764
|
+
| `leftWidth` | `number` | `244` | Deprecated;使用 `config.ui.sidebarWidth.left` |
|
|
2765
|
+
| `rightWidth` | `number` | `360` | Deprecated;使用 `config.ui.sidebarWidth.right` |
|
|
1730
2766
|
| `theme` | `NovaThemeInput` | `light` | 组件实例主题 |
|
|
1731
2767
|
| `runtimeAppearance` | `RuntimeAppearanceOptions` | 默认语义映射 | 状态、动作、转移的 Tone 映射 |
|
|
1732
|
-
| `svgExport` | `SvgExportOptions & Renderer Adapters` | `{}` |
|
|
2768
|
+
| `svgExport` | `SvgExportOptions & Renderer Adapters` | `{}` | Deprecated;使用 `config.export` |
|
|
1733
2769
|
| `iconRegistry` | `IconRegistry` | 默认 Registry | 图标覆盖入口 |
|
|
1734
2770
|
| `paletteRegistry` | `PaletteRegistry` | 默认 Registry | Palette Provider 入口 |
|
|
1735
2771
|
| `propertiesRegistry` | `PropertiesRegistry` | 默认 Registry | Properties Provider 入口 |
|
|
@@ -1738,15 +2774,27 @@ Controller 不创建 DOM,负责模型、命令、历史、选择与子流程
|
|
|
1738
2774
|
| `rendererOptions` | `DiagramRendererOptions` | `{}` | 节点与 Runtime Renderer 配置 |
|
|
1739
2775
|
| `nodeSubtitleResolver` | `NodeSubtitleResolver` | 无 | 覆盖 Design/Viewer 标准节点副标题;不影响 Instance |
|
|
1740
2776
|
| `slots` | `StudioShellSlots` | `{}` | 局部 UI 替换 |
|
|
1741
|
-
| `regions` | `StudioShellRegions` |
|
|
2777
|
+
| `regions` | `StudioShellRegions` | 按模式决定 | Deprecated;使用 `config.ui.regions`;Instance 默认隐藏右侧 |
|
|
1742
2778
|
| `layout` | `Function` | 默认三栏布局 | 完整布局替换 |
|
|
1743
2779
|
| `onThemeChange` | `(state) => void` | 无 | 主题模式解析变化回调 |
|
|
1744
2780
|
|
|
1745
|
-
实例方法包括 `getMode()`、`getAllowedModes()`、`setMode()`、`setAllowedModes()`、`subscribeMode()`、`validate()`、`subscribeValidation()`、`getRegions()`、`setRegions()`、`setRuntime()`、`setProjection()`、`refreshPresentation()`、`setTheme()`、`setRuntimeAppearance()`、`fitView()`、`zoomBy()`、`exportSvg()`、`openSvgExportPreview()` 和 `destroy()`。`setMode()` 仅在真实成功切换时返回 `true`;Mode Event 的 `source` 为 `toolbar | api | allowed-modes`。
|
|
2781
|
+
实例方法包括 `getConfig()`、`setConfig()`、`getMode()`、`getAllowedModes()`、`setMode()`、`setAllowedModes()`、`subscribeMode()`、`validate()`、`subscribeValidation()`、`getRegions()`、`setRegions()`、`setRuntime()`、`setProjection()`、`refreshPresentation()`、`setTheme()`、`setRuntimeAppearance()`、`fitView()`、`zoomBy()`、`exportSvg()`、`openSvgExportPreview()` 和 `destroy()`。`setMode()` 仅在真实成功切换时返回 `true`;Mode Event 的 `source` 为 `toolbar | api | allowed-modes`。
|
|
2782
|
+
|
|
2783
|
+
`0.3.5-preview` 新增 `getSidebarState()`、`setSidebarCollapsed(side, collapsed)`、`subscribeSidebarChange()`、`getPanelSelection()`、`subscribePanelSelection()`。侧栏状态与选择快照的类型、返回值和事件见 [API](API.md#侧栏配置状态与选择),完整框架示例见[侧栏指南](CUSTOMIZATION.md#侧栏布局与平滑折叠)。
|
|
2784
|
+
|
|
2785
|
+
| `config.ui` 配置 | 默认值 | 含义 |
|
|
2786
|
+
| --- | --- | --- |
|
|
2787
|
+
| `leftPanel.collapsible` / `rightPanel.collapsible` | `true` | 显示分隔线中点折叠按钮;`false` 时可见面板保持展开 |
|
|
2788
|
+
| `leftPanel.defaultCollapsed` / `rightPanel.defaultCollapsed` | `false` | 宽屏初始化状态;不随 Config 更新重置 |
|
|
2789
|
+
| `rightPanel.layout` | `'flex'` | `'flex'` 由宿主安排内部滚动;`'scroll'` 由 Nova 滚动整块内容 |
|
|
2790
|
+
|
|
2791
|
+
首次进入 ≤720px 的 Shell 容器时,可折叠面板默认收起且保留按钮,宽窄屏分别记忆状态。折叠动画为 200ms,支持减少动态效果,不卸载 Slot、不执行 Fit。Design 默认左右可见,Viewer 仅右侧可见,Instance 默认两侧隐藏;显式 `regions.right: 'default'` 可在 Instance 开启,`'hidden'` 隐藏整个区域和按钮,不能用右侧 Slot 覆盖。
|
|
2792
|
+
|
|
2793
|
+
`BpmnStudioConfig` 分为四组:`modeling`(Profile 和节点/连线白名单)、`ui`(`controlSize`、Regions、侧栏宽度、折叠与内容布局)、`viewer`(响应式投影、投影选项、Timeline/Details/Trace)和 `export`。`setConfig()` 先完整校验,再原地更新 UI、Viewer 与 Export,不重建 Canvas。`controlSize` 默认 `medium`,接受 `small | medium | large | '24px'…'48px'`;`null`、裸数字、其他单位及范围外值会抛出包含 `config.ui.controlSize` 的错误。
|
|
1746
2794
|
|
|
1747
2795
|
`shell.actions` 是默认 Header 与宿主自定义 Header 共用的稳定 Interface,提供 `undo()`、`redo()`、`beautify()`、`rerouteEdges()`、`fitView()`、`validate()`、`importXml()`、`exportXml()`、`exportSvg()` 和 `openSvgExportPreview()`。它不包含保存、发布、权限或文件上传等宿主业务动作。
|
|
1748
2796
|
|
|
1749
|
-
`slots.headerStart` 只替换 Nova Brand;`slots.headerActions` 只替换默认“校验 / 导入 / 导出”动作组;`slots.header` 完整替换 Header,且优先级更高。Slot Context 提供 `studio`、`shell`、`canvas`、`actions`、`getState()`、`subscribe()`、`getMode()`、`getAllowedModes()`、`subscribeMode()` 和 `subscribeValidation()
|
|
2797
|
+
`slots.headerStart` 只替换 Nova Brand;`slots.headerActions` 只替换默认“校验 / 导入 / 导出”动作组;`slots.header` 完整替换 Header,且优先级更高。Slot Context 提供 `studio`、`shell`、`canvas`、`actions`、只读 `ui`、`getState()`、`subscribe()`、`getMode()`、`getAllowedModes()`、`subscribeMode()` 和 `subscribeValidation()`。`ui.controlSize` 是语义值,`ui.controlHeight` 是归一化 px 高度。默认 Header 的最佳视图只保留在底部缩放区。`regions.header = 'hidden'` 的优先级最高。`layout()` 与 `regions` 不能同时使用。
|
|
1750
2798
|
|
|
1751
2799
|
`actions.validate()` 先更新 Nova 默认状态栏,再发送一次来源为 `toolbar` 的 Validation Event 并返回 issues;实例 `validate()` 的来源为 `api`。事件 `valid` 只由 error 决定,warning 不默认阻止发布。
|
|
1752
2800
|
|
|
@@ -1773,6 +2821,8 @@ new BpmnDesigner(options)
|
|
|
1773
2821
|
|
|
1774
2822
|
## `BpmnViewer`
|
|
1775
2823
|
|
|
2824
|
+
审批轨迹的完整输入、Vue/React/Core 加载刷新和点击事件示例见[审批轨迹接入指南](RUNTIME-INTEGRATION.md)。`runtime` 是顶层实例数据;完整 Studio 的展示配置使用 `config.viewer`,独立 Viewer 保持下表的顶层 Options。指南同时说明自 `0.3.5-preview` 提供的 Adapter Runtime 类型出口与旧版兼容推导。
|
|
2825
|
+
|
|
1776
2826
|
| 参数 | 类型 | 必填/默认 | 说明 |
|
|
1777
2827
|
| --- | --- | --- | --- |
|
|
1778
2828
|
| `container` | `HTMLElement` | 必填 | Viewer 容器 |
|
|
@@ -1854,6 +2904,7 @@ new BpmnDesigner(options)
|
|
|
1854
2904
|
| --- | --- |
|
|
1855
2905
|
| `studio` | 外部 Controller;不传时根据 `model/xml/engine` 创建并托管 |
|
|
1856
2906
|
| `model` / `xml` / `engine` | 初始及后续受控模型输入 |
|
|
2907
|
+
| `config` | `modeling/ui/viewer/export` 结构化静态配置;属性变化原地应用 Shell 配置 |
|
|
1857
2908
|
| `propertiesProfile` | 创建内部 Controller 时的属性 Profile |
|
|
1858
2909
|
| `allowedNodeTypes` | 创建内部 Controller 时使用的完整模型节点白名单 |
|
|
1859
2910
|
| `allowedEdgeTypes` | 创建内部 Controller 时使用的完整模型连线白名单 |
|
|
@@ -1861,14 +2912,17 @@ new BpmnDesigner(options)
|
|
|
1861
2912
|
| `mode` / `onModeChange` | 受控输入与实际 Mode 事件;Header Render Context 同步提供 `mode` |
|
|
1862
2913
|
| `nodeSubtitleResolver` | Design/Viewer 标准节点副标题 Resolver |
|
|
1863
2914
|
| `contextMenuRegistry` | 自定义右键动作 Registry |
|
|
1864
|
-
| `regions` |
|
|
1865
|
-
| `headerStart` / `headerActions` / `header` | React Node 或 Render Prop;使用 Portal 保留宿主 Context |
|
|
2915
|
+
| `regions` | Deprecated 兼容别名;使用 `config.ui.regions` |
|
|
2916
|
+
| `headerStart` / `headerActions` / `header` / `right` | React Node 或 Render Prop;使用 Portal 保留宿主 Context;`right` 仅替换侧栏内容 |
|
|
1866
2917
|
| `onValidation` | Nova 内部校验展示完成后的只读结果事件 |
|
|
2918
|
+
| `onSidebarChange` | 只读侧栏折叠事件,来源 `button \| api \| responsive \| config` |
|
|
1867
2919
|
| `onChange(model, reason, xml)` | 模型变化 |
|
|
1868
2920
|
| `onSelectionChange(selection, element)` | 选择变化 |
|
|
1869
2921
|
| `onScopeChange(activeScopeId, scopePath, state)` | 子流程作用域变化 |
|
|
1870
2922
|
|
|
1871
|
-
Ref:`studio`、`shell`、`actions`、`mode`、`getMode()`、`setMode()`、`setAllowedModes()`、`validate()`、`refreshPresentation()`、`exportXml()`、`fitView()`、`setTheme()`。
|
|
2923
|
+
Ref:`studio`、`shell`、`actions`、`mode`、`getConfig()`、`setConfig()`、`getMode()`、`setMode()`、`setAllowedModes()`、`validate()`、`refreshPresentation()`、`exportXml()`、`fitView()`、`setTheme()`。Header Render Context 额外提供只读 `ui`。
|
|
2924
|
+
|
|
2925
|
+
Studio Ref 同时提供五个侧栏/面板选择方法:`getSidebarState()`、`setSidebarCollapsed()`、`subscribeSidebarChange()`、`getPanelSelection()`、`subscribePanelSelection()`。Header/Right Render Context 共享响应式 `state`、`mode`、`ui`、`panelSelection`,以及 `studio`、`shell`、`actions`;只读选择使用 `panelSelection`,不将 `state.selection` 误当作轨迹当前选择。
|
|
1872
2926
|
|
|
1873
2927
|
`xml`/`model` 是外部替换输入。回写与最近导出完全相同的 XML 不会重复导入或清空 Undo/Redo;不同 XML 会替换模型和历史。React/Vue 包均重新导出创建外部 Controller 与 Palette、Properties、Context Menu、Icon、Template Registry 所需的公开函数。
|
|
1874
2928
|
|
|
@@ -1888,11 +2942,13 @@ Props:`model`、`xml`、`engine`、`theme`、`onChange`、`onSelectionChange`
|
|
|
1888
2942
|
|
|
1889
2943
|
Vue 组件与 React 使用同一底层接口,主要差异为:
|
|
1890
2944
|
|
|
1891
|
-
- Studio 提供原生 `#header-start`、`#header-actions` 和 `#
|
|
2945
|
+
- Studio 提供原生 `#header-start`、`#header-actions`、`#header` 和 `#right` Slot,通过 Teleport 保留宿主 provide/inject、响应式状态和生命周期;同名原生 Slot 高于 Core DOM Slot。
|
|
1892
2946
|
- `slotsConfig` 继续保留,作为高级 Core DOM Slot 入口;完整 Header 高于局部 Header Slots,原生局部 Slot 高于对应 Core Slot。
|
|
1893
|
-
- 事件为 `change`、`selection-change`、`scope-change`、`element-click`、`trace-click`、`update:mode`、`mode-change`、`validation`;支持 `v-model:mode`。
|
|
2947
|
+
- 事件为 `change`、`selection-change`、`scope-change`、`element-click`、`trace-click`、`update:mode`、`mode-change`、`validation`、`sidebar-change`;支持 `v-model:mode`。
|
|
1894
2948
|
- `BpmnDesigner` 发出 `change`、`selection-change`。
|
|
1895
2949
|
- Header Slot Context 的 `mode` 是响应式实际状态。Ref Expose 使用 `getStudio()`、`getShell()`、`getActions()`、`getMode()`、`setMode()`、`setAllowedModes()`、`validate()`、`refreshPresentation()` 或 `getInstance()`,不直接暴露可替换字段。
|
|
2950
|
+
- Studio Prop 与 `useBpmnStudio()` 支持 `config`;Expose 提供 `getConfig()` / `setConfig()`,Header Slot Context 提供响应式只读 `ui`。
|
|
2951
|
+
- Studio Expose 提供与 React 相同的五个侧栏/面板选择方法;Header/Right Slot Context 共享响应式 `panelSelection`。
|
|
1896
2952
|
|
|
1897
2953
|
Vue 提供 `BpmnStudio`、`BpmnDesigner`、`BpmnViewer`、`BpmnCanvas`、`BpmnPalettePanel`、`BpmnPropertiesPanel` 和 `useBpmnStudio()`。独立 `BpmnPalettePanel` 复用外部 Studio;传入 Registry 时不会隐式注册 Providers,未传 Registry 时才用 Providers 创建默认 Registry。
|
|
1898
2954
|
|
|
@@ -1916,7 +2972,7 @@ Runtime Details、Timeline 和 Transition Details 组件通过对应 `createReac
|
|
|
1916
2972
|
|
|
1917
2973
|
<!-- SOURCE: docs/API.md -->
|
|
1918
2974
|
|
|
1919
|
-
# BPMN Nova v0.3.
|
|
2975
|
+
# BPMN Nova v0.3.5 Preview API
|
|
1920
2976
|
|
|
1921
2977
|
## Framework Adapter 类型门面
|
|
1922
2978
|
|
|
@@ -1934,6 +2990,135 @@ import type {
|
|
|
1934
2990
|
|
|
1935
2991
|
React 使用相同名称,并将入口替换为 `@bpmn-nova/react`。这些是 TypeScript-only export,不会增加 Adapter 的浏览器运行时代码。
|
|
1936
2992
|
|
|
2993
|
+
### Runtime 类型门面
|
|
2994
|
+
|
|
2995
|
+
自 `0.3.5-preview` 起,Vue/React 根入口新增显式 `export type`:`ProcessInstanceSnapshot`、`ActivityInstance`、`ActivityStatus`、`RuntimeParticipant`、`RuntimeApprovalAction`、`RuntimeApprovalActionType`、`RuntimeApprovalContent`、`RuntimeApprovalContentBlock`、`RuntimeAssetRef`、`RuntimeTransition`、`RuntimeEdgeVisit`、`RuntimeTraceClickEvent`、`RuntimeAssetResolver`、`RuntimeAssetPurpose`。它们复用现有声明,不是新增 JavaScript API;Studio 原有出口保持不变。
|
|
2996
|
+
|
|
2997
|
+
真实数据转换、ID 关联、完整场景与已发布版本的 Props 类型推导见[审批轨迹接入指南](RUNTIME-INTEGRATION.md)。
|
|
2998
|
+
|
|
2999
|
+
## Studio Config Interface
|
|
3000
|
+
|
|
3001
|
+
```ts
|
|
3002
|
+
type StudioControlSize = 'small' | 'medium' | 'large' | `${number}px`
|
|
3003
|
+
|
|
3004
|
+
interface StudioSidebarConfig {
|
|
3005
|
+
collapsible?: boolean
|
|
3006
|
+
defaultCollapsed?: boolean
|
|
3007
|
+
}
|
|
3008
|
+
|
|
3009
|
+
interface StudioRightPanelConfig extends StudioSidebarConfig {
|
|
3010
|
+
layout?: 'flex' | 'scroll'
|
|
3011
|
+
}
|
|
3012
|
+
|
|
3013
|
+
interface StudioUiConfig {
|
|
3014
|
+
controlSize?: StudioControlSize
|
|
3015
|
+
regions?: StudioShellRegions
|
|
3016
|
+
sidebarWidth?: { left?: number; right?: number }
|
|
3017
|
+
leftPanel?: StudioSidebarConfig
|
|
3018
|
+
rightPanel?: StudioRightPanelConfig
|
|
3019
|
+
}
|
|
3020
|
+
|
|
3021
|
+
interface BpmnStudioConfig {
|
|
3022
|
+
modeling?: {
|
|
3023
|
+
propertiesProfile?: 'business' | 'developer'
|
|
3024
|
+
allowedNodeTypes?: readonly NodeType[]
|
|
3025
|
+
allowedEdgeTypes?: readonly EdgeType[]
|
|
3026
|
+
}
|
|
3027
|
+
ui?: StudioUiConfig
|
|
3028
|
+
viewer?: {
|
|
3029
|
+
responsive?: boolean
|
|
3030
|
+
projectionOptions?: readonly { value: ViewerProjection; label: string }[]
|
|
3031
|
+
timeline?: ViewerTimelineOptions
|
|
3032
|
+
runtimeDetails?: RuntimeDetailsOptions
|
|
3033
|
+
runtimeTraceOptions?: RuntimeTraceProjectionOptions
|
|
3034
|
+
}
|
|
3035
|
+
export?: StudioSvgExportOptions
|
|
3036
|
+
}
|
|
3037
|
+
|
|
3038
|
+
interface StudioUiContext {
|
|
3039
|
+
readonly controlSize: StudioControlSize
|
|
3040
|
+
readonly controlHeight: `${number}px`
|
|
3041
|
+
}
|
|
3042
|
+
```
|
|
3043
|
+
|
|
3044
|
+
同一个 `config` 可同时传给 `createStudioController()` 和 `createStudioShell()`;React/Vue `BpmnStudio` 与 `useBpmnStudio()` 也接受它。`config.modeling` 在 Controller 创建时建立 Profile 与建模不变量;Shell 的 `setConfig()` 只原地更新 UI、Viewer 和 Export,不改节点/连线白名单、不重建 Canvas。
|
|
3045
|
+
|
|
3046
|
+
```js
|
|
3047
|
+
const next = shell.setConfig({
|
|
3048
|
+
ui: { controlSize: '36px', regions: { right: 'hidden' } },
|
|
3049
|
+
viewer: { responsive: true },
|
|
3050
|
+
export: { padding: 36 },
|
|
3051
|
+
})
|
|
3052
|
+
|
|
3053
|
+
console.log(shell.getConfig(), shell.ui.controlHeight)
|
|
3054
|
+
```
|
|
3055
|
+
|
|
3056
|
+
`controlSize` 默认 `medium`。预设普通按钮高度是 `small=28px`、`medium=32px`、`large=40px`;自定义值严格接受 24px–48px。非法配置在任何 DOM 写入前抛错,错误包含 `config.ui.controlSize`。尺寸只影响 Header。Core Slot、Vue Slot 和 React Render Context 都提供只读 `ui`;自定义 Header 还可以继承 `--nova-control-height`、`--nova-header-height`、`--nova-control-font-size`、`--nova-control-padding-inline`、`--nova-control-icon-size` 与 `--nova-control-radius`。
|
|
3057
|
+
|
|
3058
|
+
旧顶层字段继续兼容并标记 Deprecated:
|
|
3059
|
+
|
|
3060
|
+
| 旧字段 | Config 路径 |
|
|
3061
|
+
| --- | --- |
|
|
3062
|
+
| `propertiesProfile` / `allowedNodeTypes` / `allowedEdgeTypes` | `config.modeling.*` |
|
|
3063
|
+
| `regions` / `leftWidth` / `rightWidth` | `config.ui.regions` / `config.ui.sidebarWidth.*` |
|
|
3064
|
+
| `responsive` / `projectionOptions` / `timeline` / `runtimeDetails` / `runtimeTraceOptions` | `config.viewer.*` |
|
|
3065
|
+
| `svgExport` | `config.export` |
|
|
3066
|
+
|
|
3067
|
+
同一项同时出现时,旧顶层显式值优先,以保证现有接入无行为变化。
|
|
3068
|
+
|
|
3069
|
+
### 侧栏配置、状态与选择
|
|
3070
|
+
|
|
3071
|
+
自 `0.3.5-preview` 起,新增侧栏类型和接口从 Studio/Vue/React 根声明入口提供,不要求增加其他直接依赖。`config.ui.leftPanel/rightPanel.collapsible` 默认 `true`;`defaultCollapsed` 只用于宽屏初始化,默认 `false`。`rightPanel.layout` 默认 `flex`,宿主控制内部滚动;`scroll` 由 Nova 负责整块内容滚动。完整示例见[侧栏布局与平滑折叠](CUSTOMIZATION.md#侧栏布局与平滑折叠)。
|
|
3072
|
+
|
|
3073
|
+
```ts
|
|
3074
|
+
type StudioSidebarSide = 'left' | 'right'
|
|
3075
|
+
type StudioSidebarChangeSource = 'button' | 'api' | 'responsive' | 'config'
|
|
3076
|
+
|
|
3077
|
+
interface StudioSidebarChangeEvent {
|
|
3078
|
+
readonly side: StudioSidebarSide
|
|
3079
|
+
readonly collapsed: boolean
|
|
3080
|
+
readonly previousCollapsed: boolean
|
|
3081
|
+
readonly mode: StudioMode
|
|
3082
|
+
readonly source: StudioSidebarChangeSource
|
|
3083
|
+
}
|
|
3084
|
+
```
|
|
3085
|
+
|
|
3086
|
+
| Shell 方法 | 契约 |
|
|
3087
|
+
| --- | --- |
|
|
3088
|
+
| `getSidebarState()` | 返回只读左右状态;每侧包含 `collapsed`、`hidden`、`collapsible` |
|
|
3089
|
+
| `setSidebarCollapsed(side, collapsed): boolean` | 仅实际切换返回 `true`;隐藏、禁用折叠、销毁或重复设置返回 `false` |
|
|
3090
|
+
| `subscribeSidebarChange(listener)` | 订阅只读状态变化;返回取消订阅函数,不按动画帧发送 |
|
|
3091
|
+
| `getPanelSelection()` | 返回 `{ selection, selectedElement, trace }`,只读空选择的三个字段均为 `null` |
|
|
3092
|
+
| `subscribePanelSelection(listener)` | 接收面板选择快照;返回取消订阅函数 |
|
|
3093
|
+
|
|
3094
|
+
Design 的面板选择沿用 Controller 选择语义(包括默认流程选择),Viewer/Instance 来自实际节点/轨迹点击,不改变设计选择。Model/Runtime 更新后重新解析;模式切换成功后才更新。Core Slot 只挂载一次,通过订阅响应变化;Vue/React Header 与 Right Context 直接提供响应式 `panelSelection`。
|
|
3095
|
+
|
|
3096
|
+
框架 Studio Expose/Handle 透传上述方法;侧栏事件分别为 Vue `sidebar-change`、React `onSidebarChange`。新 Vue `#right` 与 React `right` 优先于对应 Core DOM Slot,只替换右侧内容,不重建 Canvas。
|
|
3097
|
+
|
|
3098
|
+
默认 Design 显示左右,Viewer 仅显示右侧,Instance 两侧隐藏。未配置 `regions.right` 时采用模式默认;显式 `'default'` 可在 Instance 开启右侧,`'hidden'` 隐藏内容及按钮。`getConfig()` 保留未配置与显式配置的区别,`setConfig(getConfig())` 不改变这一意图。隐藏与收起独立,Slot 不会自动开启隐藏的侧栏。
|
|
3099
|
+
|
|
3100
|
+
Shell 容器宽度 ≤720px 为窄屏,首次进入时可折叠侧栏收起;宽/窄屏分别记忆状态。两侧可同时展开并挤压画布;收起和展开采用 200ms 过渡,尊重减少动态效果,不自动 Fit。`sidebarWidth` 为展开首选宽度,窄空间按比例约束以保留画布。折叠状态不写入 Model、XML、History 或持久化存储。自定义完整 `layout()` 和独立 Designer/Viewer 不使用这套默认侧栏。
|
|
3101
|
+
|
|
3102
|
+
## 节点 DI 与视觉尺寸
|
|
3103
|
+
|
|
3104
|
+
`createNode()` / Palette 使用类型定义的默认宽高;加载已有 XML(包括组件接收服务端返回的 `xml`)使用 BPMN DI Bounds。XML 导出已写入节点的 `x/y/width/height`,并沿用既有取整与 waypoint 序列化规则;小尺寸节点的外壳溢出不是“保存遗漏宽高”。
|
|
3105
|
+
|
|
3106
|
+
以下标准外壳的适配自 `0.3.5-preview` 提供,不新增公共 Interface 或尺寸配置:
|
|
3107
|
+
|
|
3108
|
+
| 标准图元 | 默认节点容器 | 内部图形基准 | 显示缩放比例 |
|
|
3109
|
+
| --- | --- | --- | --- |
|
|
3110
|
+
| 排他、并行、包容、事件、复杂网关 | 72×72 | 49×49,旋转 45° | `min(1, width / (49√2), height / (49√2))` |
|
|
3111
|
+
| 数据对象 | 72×88 | 50×62 | `min(1, width / 50, height / 62)` |
|
|
3112
|
+
| 数据存储 | 78×78 | 56×50 | `min(1, width / 56, height / 50)` |
|
|
3113
|
+
|
|
3114
|
+
图形及其描边、圆角、折角和内部标记等比缩小、不放大,并相对节点容器居中。标签不随图形缩放,水平中心对齐容器,顶部位于容器底部下方 6px。并行网关的角色徽标保持独立。事件沿用已有尺寸约束修复;任务、子流程和泳道不在此次调整范围。
|
|
3115
|
+
|
|
3116
|
+
DOM Canvas/Viewer/Instance 与纯 SVG Export 共享内部 `node-geometry` Module,以实际渲染 Projection Model 为输入。仅复制显示路径并将首末非零线段与标准外轮廓求交;中间折点、方向、连线标签基准及持久化 waypoint 不变。命中区域使用同一显示路径,折点编辑仍操作原始数据;已贴边目标的箭头尖端与参考点重合。已有连接手柄按可见轮廓定位,不改变连接规则。
|
|
3117
|
+
|
|
3118
|
+
非有限或非正尺寸不产生标准外壳几何;无合法交点、退化路径或 SVG 自定义 Renderer 成功接管的节点不做标准端点校正。SVG Renderer 每个节点只调用一次;返回 `false` 或抛错时继续原有标准降级/警告规则。该 Module 不作为公共 npm 包或 API 导出。宿主无需扩大 DI、删除重建节点、覆盖内部 CSS 或为此迁移服务端 XML。
|
|
3119
|
+
|
|
3120
|
+
这里的不变性针对重绘、展示刷新和 SVG 导出,不是对所有编辑操作的无损承诺:现有 Controller `updateNode()` 会清空该节点相邻连线的 waypoint(仅改名也会触发),随后沿用 Core 自动路由。本轮没有改变该编辑语义;也不承诺 XML 的字节级往返一致性。
|
|
3121
|
+
|
|
1937
3122
|
## Designer
|
|
1938
3123
|
|
|
1939
3124
|
```js
|
|
@@ -2126,12 +3311,12 @@ interface StudioShellRegions {
|
|
|
2126
3311
|
const shell = createStudioShell({
|
|
2127
3312
|
container,
|
|
2128
3313
|
studio,
|
|
2129
|
-
regions: { right: 'hidden' },
|
|
3314
|
+
config: { ui: { regions: { right: 'hidden' } } },
|
|
2130
3315
|
slots: {
|
|
2131
|
-
headerStart({ container, studio, shell, canvas, actions, getState, subscribe, getMode, getAllowedModes, subscribeMode, subscribeValidation }) {
|
|
3316
|
+
headerStart({ container, studio, shell, canvas, actions, getState, subscribe, getMode, getAllowedModes, subscribeMode, subscribeValidation, ui }) {
|
|
2132
3317
|
// 只替换 Brand;默认模式区和编辑工具继续保留。
|
|
2133
3318
|
},
|
|
2134
|
-
headerActions({ container, actions, getMode, subscribeMode }) {
|
|
3319
|
+
headerActions({ container, actions, getMode, subscribeMode, ui }) {
|
|
2135
3320
|
// 替换默认“校验 / 导入 / 导出”,挂载宿主“校验 / 保存 / 发布”。
|
|
2136
3321
|
// 校验调用 actions.validate();保存与发布调用宿主服务。
|
|
2137
3322
|
},
|
|
@@ -2142,7 +3327,7 @@ shell.getRegions()
|
|
|
2142
3327
|
shell.setRegions({ right: 'default' })
|
|
2143
3328
|
```
|
|
2144
3329
|
|
|
2145
|
-
`slots.headerActions` 只替换 Header 最右侧默认动作组,可与 `slots.headerStart` 同时使用;`slots.header` 完整替换 Header,并高于两个局部 Slot;`regions.header = 'hidden'`
|
|
3330
|
+
`slots.headerActions` 只替换 Header 最右侧默认动作组,可与 `slots.headerStart` 同时使用;`slots.header` 完整替换 Header,并高于两个局部 Slot;`regions.header = 'hidden'` 的优先级最高。Slot Context 的 `ui` 与 Shell 实际 Config 同步。未提供 `headerActions` 时仍显示“校验 / 导入 / 导出”。顶部不再重复渲染最佳视图,底部缩放区与 `fitView()` Interface 保持不变。`layout()` 与 `regions` 互斥。
|
|
2146
3331
|
|
|
2147
3332
|
```ts
|
|
2148
3333
|
interface StudioShellActions {
|
|
@@ -2163,6 +3348,8 @@ interface StudioShellActions {
|
|
|
2163
3348
|
|
|
2164
3349
|
Vue `BpmnStudio` 提供 `#header-start` / `#header-actions` / `#header` 与 `getActions()`;React 提供 `headerStart` / `headerActions` / `header` Render Prop 与 Ref `actions`。Core Slot 可通过 `getMode()` / `subscribeMode()` / `subscribeValidation()` 观察 Shell,框架 Slot/Render Context 直接提供响应式实际 `mode`。框架插槽的 `state` 会随 Controller 事件更新,Undo/Redo 禁用状态可以直接绑定 `state.canUndo/canRedo`。
|
|
2165
3350
|
|
|
3351
|
+
右侧内容使用 Core `slots.right`、Vue `#right` 或 React `right`。所有 Header/Right Context 共用 `mode`、`state`、`ui`、`panelSelection` 及 Actions/Controller/Shell;右侧的布局和展开/收起由默认 Shell 管理,见上面的侧栏 Interface。
|
|
3352
|
+
|
|
2166
3353
|
## Group 与 Pool / Lane
|
|
2167
3354
|
|
|
2168
3355
|
`Group` 是不持有成员引用的 BPMN 视觉 Artifact;`Lane` 通过 `containerId` 表示画布上的 Participant containment,并继续使用标准 `flowNodeRefs` 表示 BPMN 成员。`containerId` 不会写入 XML,导入时会根据 Participant 的 `processRef` 与 BPMN DI Bounds 恢复。
|
|
@@ -2400,6 +3587,8 @@ Viewer 与审批轨迹模式可在空白画布上直接按住左键拖动视口
|
|
|
2400
3587
|
|
|
2401
3588
|
### Runtime presentation
|
|
2402
3589
|
|
|
3590
|
+
引擎映射和组件接入步骤集中在[审批轨迹接入指南](RUNTIME-INTEGRATION.md)。特别注意当前 `invalidatedActivityIds` 表示 BPMN 节点 ID,而 `sourceActivityId` 表示工作项实例 ID;两者不能互换。
|
|
3591
|
+
|
|
2403
3592
|
Runtime snapshots are engine-neutral. `visitId` separates repeated arrivals at the same BPMN element; records sharing a visit describe one approval round, while `multiInstanceId` and an explicit `approvalMode` describe multi-person approval. Multiple people are never assumed to be countersign unless the snapshot declares `approvalMode: 'all'`.
|
|
2404
3593
|
|
|
2405
3594
|
```js
|
|
@@ -2621,12 +3810,21 @@ Stage 0 将编辑器拆成无 UI 的 `BpmnStudioController`,以及可组合的
|
|
|
2621
3810
|
## 默认组合
|
|
2622
3811
|
|
|
2623
3812
|
```js
|
|
3813
|
+
const config = {
|
|
3814
|
+
modeling: { propertiesProfile: 'business' },
|
|
3815
|
+
ui: { controlSize: 'medium' },
|
|
3816
|
+
};
|
|
3817
|
+
|
|
2624
3818
|
const studio = createStudioController({
|
|
2625
3819
|
model: createSampleProcess('flowable'),
|
|
2626
|
-
|
|
3820
|
+
config,
|
|
2627
3821
|
});
|
|
2628
3822
|
|
|
2629
|
-
const shell = createStudioShell({
|
|
3823
|
+
const shell = createStudioShell({
|
|
3824
|
+
container,
|
|
3825
|
+
studio,
|
|
3826
|
+
config,
|
|
3827
|
+
});
|
|
2630
3828
|
```
|
|
2631
3829
|
|
|
2632
3830
|
NPM 接入只需要加载 Studio 的自包含样式入口。它已经按 Theme、Icons、Renderer、Palette、Properties Renderer、Studio 的顺序打包,避免宿主重复引入内部 CSS。
|
|
@@ -2709,20 +3907,20 @@ properties.registerProvider(850, {
|
|
|
2709
3907
|
|
|
2710
3908
|
## 组合 Shell 区域与宿主界面
|
|
2711
3909
|
|
|
2712
|
-
默认 Shell 包含 Header、Palette、Canvas、Properties 和 Statusbar。仅需要移除某个默认区域时使用 `regions`,不需要重写布局:
|
|
3910
|
+
默认 Shell 包含 Header、Palette、Canvas、Properties 和 Statusbar。仅需要移除某个默认区域时使用 `config.ui.regions`,不需要重写布局:
|
|
2713
3911
|
|
|
2714
3912
|
```js
|
|
2715
3913
|
const shell = createStudioShell({
|
|
2716
3914
|
container,
|
|
2717
3915
|
studio,
|
|
2718
|
-
regions: { right: 'hidden' },
|
|
3916
|
+
config: { ui: { regions: { right: 'hidden' } } },
|
|
2719
3917
|
})
|
|
2720
3918
|
|
|
2721
3919
|
// 原地切换,不重建 Controller、Canvas、历史或视口。
|
|
2722
3920
|
shell.setRegions({ right: 'default' })
|
|
2723
3921
|
```
|
|
2724
3922
|
|
|
2725
|
-
`header`、`left`、`right`、`footer` 均接受 `default | hidden`。隐藏区域不占 Grid
|
|
3923
|
+
`header`、`left`、`right`、`footer` 均接受 `default | hidden`。隐藏区域不占 Grid 轨道、不显示边框或折叠按钮,也不保留可聚焦控件。左侧只属于 Design;右侧在 Design/Viewer 默认显示,在 Instance 默认隐藏。显式设置 `regions.right: 'default'` 可在 Instance 开启右侧;仅提供右侧 Slot 不会覆盖隐藏规则。
|
|
2726
3924
|
|
|
2727
3925
|
局部内容替换使用 `slots.left`、`slots.right`、`slots.headerStart`、`slots.headerActions`、`slots.header` 和 `slots.footer`。`headerStart` 只替换默认 Brand;`headerActions` 只替换最右侧默认“校验 / 导入 / 导出”动作组;`header` 替换完整 Header,优先级高于两个局部 Slot。回调收到 `studio`、`shell`、`actions`、`getState()`、`subscribe()`、注册表、交互控制器和 Canvas。
|
|
2728
3926
|
|
|
@@ -2734,8 +3932,9 @@ createStudioShell({
|
|
|
2734
3932
|
headerStart({ container, actions, getState, subscribe }) {
|
|
2735
3933
|
// 挂载返回入口、业务图标、流程名称和类型;返回卸载函数。
|
|
2736
3934
|
},
|
|
2737
|
-
headerActions({ container, actions, getMode, subscribeMode }) {
|
|
3935
|
+
headerActions({ container, actions, getMode, subscribeMode, ui }) {
|
|
2738
3936
|
// 挂载校验 / 保存 / 发布;校验调用 actions.validate(),其余调用宿主服务。
|
|
3937
|
+
// ui.controlSize / ui.controlHeight 始终反映 Shell 的实际配置。
|
|
2739
3938
|
},
|
|
2740
3939
|
left({ container, studio, interactions, canvas }) {
|
|
2741
3940
|
// 挂载任意 Vanilla / React / Vue UI,返回卸载函数。
|
|
@@ -2759,11 +3958,226 @@ async function publishProcess(actions) {
|
|
|
2759
3958
|
}
|
|
2760
3959
|
```
|
|
2761
3960
|
|
|
2762
|
-
React 使用 `headerStart` / `headerActions` / `header` Render Prop 和 Portal;Vue 使用 `#header-start` / `#header-actions` / `#header` Named Slot 和 Teleport。两者都在宿主应用树内渲染,不创建独立 React Root 或 Vue App,因此 Context、provide/inject、响应式状态和生命周期继续有效。Vue 原有 `slotsConfig`、React 原有 `slots` 仍作为 Core DOM
|
|
3961
|
+
React 使用 `headerStart` / `headerActions` / `header` / `right` Render Prop 和 Portal;Vue 使用 `#header-start` / `#header-actions` / `#header` / `#right` Named Slot 和 Teleport。两者都在宿主应用树内渲染,不创建独立 React Root 或 Vue App,因此 Context、provide/inject、响应式状态和生命周期继续有效。Vue 原有 `slotsConfig`、React 原有 `slots` 仍作为 Core DOM 接口保留;同名框架原生 Slot 优先。右侧原生 Slot 与下述侧栏能力自 `0.3.5-preview` 提供。
|
|
3962
|
+
|
|
3963
|
+
### 侧栏布局与平滑折叠
|
|
3964
|
+
|
|
3965
|
+
Nova 管理侧栏宽度、折叠按钮和可用高度,宿主只填入内容。不要先配置 `right: 'hidden'` 再传右侧 Slot:隐藏的是整个区域;Slot 替换的只是内容。
|
|
3966
|
+
|
|
3967
|
+
```js
|
|
3968
|
+
const config = {
|
|
3969
|
+
ui: {
|
|
3970
|
+
sidebarWidth: { left: 244, right: 360 },
|
|
3971
|
+
leftPanel: { collapsible: true, defaultCollapsed: false },
|
|
3972
|
+
rightPanel: { collapsible: true, defaultCollapsed: false, layout: 'flex' },
|
|
3973
|
+
// regions: { right: 'default' }, // 仅在 Instance 也需要右侧时显式开启
|
|
3974
|
+
},
|
|
3975
|
+
}
|
|
3976
|
+
```
|
|
3977
|
+
|
|
3978
|
+
| 状态 | 侧栏占用宽度 | 分隔线中点按钮 |
|
|
3979
|
+
| --- | --- | --- |
|
|
3980
|
+
| 展开 | 是 | 收起按钮;`collapsible: false` 时不显示 |
|
|
3981
|
+
| 收起 | 否 | 展开按钮仍保留在对应边缘 |
|
|
3982
|
+
| 隐藏 | 否 | 不显示,Slot 也不可见 |
|
|
3983
|
+
|
|
3984
|
+
- `collapsible` 默认 `true`;`defaultCollapsed` 只设置宽屏初始化状态,默认 `false`。后续更新 Config 不重置用户操作;运行时设为 `collapsible: false` 会展开面板并移除按钮。
|
|
3985
|
+
- 左右独立;宽屏与窄屏分别记忆当前 Shell 生命周期内的折叠状态,不存入模型或本地存储。窄屏以 **Shell 容器宽度 ≤720px** 判断,首次进入时可折叠侧栏默认收起,保留展开按钮。
|
|
3986
|
+
- 窄屏展开仍挤压画布,不使用抽屉、遮罩或焦点陷阱,允许两侧同时展开。空间不足按首选宽度比例缩小侧栏,为画布保留至少 `min(96px, 容器宽度)`;空间恢复后还原首选宽度。
|
|
3987
|
+
- 折叠宽度采用 `200ms cubic-bezier(0.2, 0, 0, 1)`,快速反向操作从当前位置继续;初始化无动画,系统启用减少动态效果时关闭动画。按钮支持键盘和 `aria-expanded`,收起内容不可聚焦。
|
|
3988
|
+
- 收起或隐藏不卸载 Slot、不丢失表单状态与滚动位置、不重建 Canvas,也不调用 Fit 或改变 Zoom/Pan。Viewer 自动投影的尺寸响应在动画结束后按最终宽度合并处理;跨投影阈值时沿用原投影切换行为。
|
|
3989
|
+
- 完整自定义 `layout()` 不接管上述默认侧栏布局;独立 Designer/Viewer 没有 Shell 侧栏配置。
|
|
3990
|
+
|
|
3991
|
+
### Flex 或 Scroll:谁负责滚动
|
|
3992
|
+
|
|
3993
|
+
右侧 `layout: 'flex'` 为默认值:Nova 提供满宽高纵向 Flex 内容容器,限制溢出并设置 `min-width/min-height: 0`。宿主自行安排固定标题、弹性滚动内容和固定底部;不会给每个子元素强加 `flex: 1`。内置面板使用固定标题、中间内容滚动。
|
|
3994
|
+
|
|
3995
|
+
使用自己的类名即可,不覆盖 Nova 内部选择器:
|
|
3996
|
+
|
|
3997
|
+
```css
|
|
3998
|
+
.host-panel {
|
|
3999
|
+
display: flex;
|
|
4000
|
+
flex-direction: column;
|
|
4001
|
+
flex: 1;
|
|
4002
|
+
min-width: 0;
|
|
4003
|
+
min-height: 0;
|
|
4004
|
+
overflow: hidden;
|
|
4005
|
+
}
|
|
4006
|
+
.host-panel > header,
|
|
4007
|
+
.host-panel > footer { flex: 0 0 auto; padding: 12px; }
|
|
4008
|
+
.host-panel-content { flex: 1; min-height: 0; overflow: auto; padding: 12px; }
|
|
4009
|
+
```
|
|
4010
|
+
|
|
4011
|
+
如果宿主希望只填入普通内容,设置 `layout: 'scroll'`。Nova 会让整个右侧内容纵向滚动,标题也随内容滚动;此时不要再给内容包一层固定高度或滚动容器。Studio 的宿主仍须提供可计算高度,如 `height: 720px`,或一条完整、可计算的父级 Flex 高度链;仅设置一个没有高度基准的 `height: 100%` 不够。
|
|
4012
|
+
|
|
4013
|
+
### Vue:原生右侧 Slot
|
|
4014
|
+
|
|
4015
|
+
```vue
|
|
4016
|
+
<script setup>
|
|
4017
|
+
import { ref } from 'vue'
|
|
4018
|
+
import { BpmnStudio } from '@bpmn-nova/vue'
|
|
4019
|
+
import '@bpmn-nova/vue/styles.css'
|
|
4020
|
+
|
|
4021
|
+
const props = defineProps({ xml: String })
|
|
4022
|
+
const studioRef = ref(null)
|
|
4023
|
+
const note = ref('')
|
|
4024
|
+
const config = {
|
|
4025
|
+
ui: {
|
|
4026
|
+
leftPanel: { collapsible: true },
|
|
4027
|
+
rightPanel: { layout: 'flex', collapsible: true },
|
|
4028
|
+
},
|
|
4029
|
+
}
|
|
4030
|
+
function observeSidebar(event) { console.log(event.side, event.collapsed, event.source) }
|
|
4031
|
+
</script>
|
|
4032
|
+
|
|
4033
|
+
<template>
|
|
4034
|
+
<div style="height: 720px">
|
|
4035
|
+
<BpmnStudio ref="studioRef" :xml="props.xml" :config="config" @sidebar-change="observeSidebar">
|
|
4036
|
+
<template #right="{ panelSelection, mode }">
|
|
4037
|
+
<section class="host-panel">
|
|
4038
|
+
<header>业务详情 · {{ mode }}</header>
|
|
4039
|
+
<div class="host-panel-content">
|
|
4040
|
+
<p>{{ panelSelection.selectedElement?.id ?? '未选择元素' }}</p>
|
|
4041
|
+
<label>宿主备注 <textarea v-model="note" /></label>
|
|
4042
|
+
</div>
|
|
4043
|
+
<footer><button type="button" @click="studioRef?.setSidebarCollapsed('right', true)">收起</button></footer>
|
|
4044
|
+
</section>
|
|
4045
|
+
</template>
|
|
4046
|
+
</BpmnStudio>
|
|
4047
|
+
</div>
|
|
4048
|
+
</template>
|
|
4049
|
+
```
|
|
4050
|
+
|
|
4051
|
+
示例使用前面的 `.host-panel` 样式。`note` 是宿主业务状态,不会因收起而销毁,不会自动写入 BPMN。
|
|
4052
|
+
|
|
4053
|
+
### React:原生右侧 Render Prop
|
|
4054
|
+
|
|
4055
|
+
```jsx
|
|
4056
|
+
import { useRef, useState } from 'react'
|
|
4057
|
+
import { BpmnStudio } from '@bpmn-nova/react'
|
|
4058
|
+
import '@bpmn-nova/react/styles.css'
|
|
4059
|
+
|
|
4060
|
+
const config = { ui: { rightPanel: { layout: 'flex', collapsible: true } } }
|
|
4061
|
+
|
|
4062
|
+
export function WorkflowEditor({ xml }) {
|
|
4063
|
+
const studioRef = useRef(null)
|
|
4064
|
+
const [note, setNote] = useState('')
|
|
4065
|
+
return (
|
|
4066
|
+
<div style={{ height: 720 }}>
|
|
4067
|
+
<BpmnStudio
|
|
4068
|
+
ref={studioRef}
|
|
4069
|
+
xml={xml}
|
|
4070
|
+
config={config}
|
|
4071
|
+
onSidebarChange={(event) => console.log(event.side, event.collapsed, event.source)}
|
|
4072
|
+
right={({ panelSelection, mode }) => (
|
|
4073
|
+
<section className="host-panel">
|
|
4074
|
+
<header>业务详情 · {mode}</header>
|
|
4075
|
+
<div className="host-panel-content">
|
|
4076
|
+
<p>{panelSelection.selectedElement?.id ?? '未选择元素'}</p>
|
|
4077
|
+
<label>宿主备注 <textarea value={note} onChange={(event) => setNote(event.target.value)} /></label>
|
|
4078
|
+
</div>
|
|
4079
|
+
<footer><button type="button" onClick={() => studioRef.current?.setSidebarCollapsed('right', true)}>收起</button></footer>
|
|
4080
|
+
</section>
|
|
4081
|
+
)}
|
|
4082
|
+
/>
|
|
4083
|
+
</div>
|
|
4084
|
+
)
|
|
4085
|
+
}
|
|
4086
|
+
```
|
|
4087
|
+
|
|
4088
|
+
### Core:内容与选择订阅
|
|
4089
|
+
|
|
4090
|
+
```js
|
|
4091
|
+
import { createStudioController, createStudioShell, importBpmn } from '@bpmn-nova/studio'
|
|
4092
|
+
import '@bpmn-nova/studio/styles.css'
|
|
4093
|
+
|
|
4094
|
+
// container 已有可计算高度;xml 为宿主加载的流程定义。
|
|
4095
|
+
const studio = createStudioController({ model: importBpmn(xml, 'flowable') })
|
|
4096
|
+
const shell = createStudioShell({
|
|
4097
|
+
container,
|
|
4098
|
+
studio,
|
|
4099
|
+
config: { ui: { rightPanel: { layout: 'scroll' } } },
|
|
4100
|
+
slots: {
|
|
4101
|
+
right({ container: content, shell }) {
|
|
4102
|
+
const heading = document.createElement('h2')
|
|
4103
|
+
const summary = document.createElement('p')
|
|
4104
|
+
heading.textContent = '业务详情'
|
|
4105
|
+
content.append(heading, summary)
|
|
4106
|
+
const render = ({ selectedElement }) => {
|
|
4107
|
+
summary.textContent = selectedElement?.id ?? '未选择元素'
|
|
4108
|
+
}
|
|
4109
|
+
render(shell.getPanelSelection())
|
|
4110
|
+
const unsubscribe = shell.subscribePanelSelection(render)
|
|
4111
|
+
return () => { unsubscribe(); heading.remove(); summary.remove() }
|
|
4112
|
+
},
|
|
4113
|
+
},
|
|
4114
|
+
})
|
|
4115
|
+
|
|
4116
|
+
const offSidebar = shell.subscribeSidebarChange((event) => {
|
|
4117
|
+
console.log(event.side, event.previousCollapsed, event.collapsed, event.source)
|
|
4118
|
+
})
|
|
4119
|
+
shell.setSidebarCollapsed('left', true)
|
|
4120
|
+
console.log(shell.getSidebarState().left)
|
|
4121
|
+
|
|
4122
|
+
// 宿主卸载时执行:offSidebar(); shell.destroy(); studio.destroy()
|
|
4123
|
+
```
|
|
4124
|
+
|
|
4125
|
+
### 折叠状态与面板选择契约
|
|
4126
|
+
|
|
4127
|
+
`getSidebarState()` 返回左右两侧各自的 `collapsed`、`hidden`、`collapsible`。`setSidebarCollapsed(side, collapsed)` 仅在实际成功改变时返回 `true`;隐藏、禁用折叠、销毁或重复设置返回 `false`。`subscribeSidebarChange()` 返回取消订阅函数,事件为只读 `{ side, collapsed, previousCollapsed, mode, source }`;`source` 为 `button | api | responsive | config`,不会按动画帧重复发送。Vue Expose/React Handle 透传三个方法,组件事件分别为 `sidebar-change`/`onSidebarChange`。
|
|
4128
|
+
|
|
4129
|
+
所有框架 Header/Right Context 共用响应式 `mode`、`state`、`ui`、`panelSelection`,以及 `studio`、`shell`、`actions`。`panelSelection` 为 `{ selection, selectedElement, trace }`:Design 从 Controller 选择派生;Viewer/Instance 从实际点击派生,包含适用的轨迹上下文,不回写设计选择。空白点击清空;Model/Runtime 更新后重新解析,避免展示旧实例事实。Core 通过 `getPanelSelection()` / `subscribePanelSelection()` 使用相同状态。
|
|
4130
|
+
|
|
4131
|
+
`state.selection` 是设计选择,不应作为只读轨迹当前选择的替代。业务字段仍以稳定 BPMN ID 关联;需要审批动作、附件或跳转事实时读取 `trace` 或既有 `trace-click`,而不是根据属性面板可见性判断。隐藏右侧不关闭轨迹详情、点击事件或附件能力。
|
|
4132
|
+
|
|
4133
|
+
### Header 控件尺寸与宿主 UI 库
|
|
4134
|
+
|
|
4135
|
+
通过 `config.ui.controlSize` 统一调整顶部 Header 的 Mode、Projection、撤销/重做、美化、默认动作组和自定义 Header 容器。默认值是 `medium`;预设与常见宿主组件库的语义映射如下:
|
|
4136
|
+
|
|
4137
|
+
| Nova | 普通控件高度 | Element Plus | Ant Design |
|
|
4138
|
+
| --- | ---: | --- | --- |
|
|
4139
|
+
| `small` | 28px | `small` | `small` |
|
|
4140
|
+
| `medium` | 32px | `default` | `middle` |
|
|
4141
|
+
| `large` | 40px | `large` | `large` |
|
|
4142
|
+
|
|
4143
|
+
宿主设计系统不落在预设上时,可传严格的 `24px`–`48px` CSS 像素字符串:
|
|
4144
|
+
|
|
4145
|
+
```js
|
|
4146
|
+
const config = {
|
|
4147
|
+
ui: {
|
|
4148
|
+
controlSize: '36px',
|
|
4149
|
+
regions: { right: 'hidden' },
|
|
4150
|
+
sidebarWidth: { left: 256, right: 384 },
|
|
4151
|
+
},
|
|
4152
|
+
}
|
|
4153
|
+
|
|
4154
|
+
const shell = createStudioShell({ container, studio, config })
|
|
4155
|
+
shell.setConfig({ ...shell.getConfig(), ui: { ...shell.getConfig().ui, controlSize: 'large' } })
|
|
4156
|
+
```
|
|
4157
|
+
|
|
4158
|
+
`setConfig()` 先完整校验再更新现有 Shell,不重建 Canvas,也不改变 Model、XML、History、Selection、Mode 或 Viewport。传入 `null`、裸数字、`rem`、任意 CSS 表达式或范围外值会抛出包含 `config.ui.controlSize` 的错误。`config.modeling.allowedNodeTypes` 和 `allowedEdgeTypes` 是 Controller 初始化不变量;运行中只有 Profile 继续通过 `setPropertiesProfile()` 修改。
|
|
4159
|
+
|
|
4160
|
+
自定义 Header Actions 可通过 Context 的只读 `ui.controlSize` / `ui.controlHeight` 映射宿主组件尺寸,并继承以下 CSS Variables:
|
|
4161
|
+
|
|
4162
|
+
```css
|
|
4163
|
+
.host-studio-actions .host-button {
|
|
4164
|
+
min-height: var(--nova-control-height);
|
|
4165
|
+
padding-inline: var(--nova-control-padding-inline);
|
|
4166
|
+
border-radius: var(--nova-control-radius);
|
|
4167
|
+
font-size: var(--nova-control-font-size);
|
|
4168
|
+
}
|
|
4169
|
+
|
|
4170
|
+
.host-studio-actions .host-button svg {
|
|
4171
|
+
width: var(--nova-control-icon-size);
|
|
4172
|
+
height: var(--nova-control-icon-size);
|
|
4173
|
+
}
|
|
4174
|
+
```
|
|
4175
|
+
|
|
4176
|
+
还可读取 `--nova-header-height`。这些变量只控制 Header;底部缩放区、画布浮动工具、Palette 和 Properties 保持自己的尺寸。旧顶层 `propertiesProfile`、`allowedNodeTypes`、`allowedEdgeTypes`、`regions`、`leftWidth`、`rightWidth`、`responsive`、`projectionOptions`、`timeline`、`runtimeDetails`、`runtimeTraceOptions` 与 `svgExport` 仍作为 Deprecated 兼容别名;新旧同时出现时,旧顶层显式值优先。
|
|
2763
4177
|
|
|
2764
4178
|
若整个三栏结构都要自定义,使用 `layout({ container, mount, ...services })`。`mount.canvas()`、`mount.palette()`、`mount.properties()` 可分别挂载官方实现,也可只使用 Controller 自行实现。`layout()` 与 `regions` 是互斥 Interface,同时传入会抛出配置错误。
|
|
2765
4179
|
|
|
2766
|
-
|
|
4180
|
+
宿主业务属性面板既可通过右侧 Slot 复用 Nova 布局,也可显式隐藏右侧并放在 Nova 根节点外部。外部设计面板订阅 Controller 的 `selectionChanged`(框架中为 `selection-change` / `onSelectionChange`);该事件覆盖节点、连线、多选、清空和键盘选择,`element-click` 不能替代它。跨模式的面板使用上述 `panelSelection`。业务配置以稳定 BPMN Element ID 关联,不写入 Nova UI 状态。
|
|
2767
4181
|
|
|
2768
4182
|
## 自定义画布右键菜单
|
|
2769
4183
|
|
|
@@ -2808,6 +4222,12 @@ createStudioShell({
|
|
|
2808
4222
|
|
|
2809
4223
|
`rendererOptions.nodeRenderers` 按节点类型或节点 kind 注册内部渲染器。外框、端口、选择、连接、拖动和删除仍由 Canvas 管理。
|
|
2810
4224
|
|
|
4225
|
+
### 标准外壳与导入尺寸
|
|
4226
|
+
|
|
4227
|
+
Palette 默认节点尺寸与 XML 中的 DI Bounds 可以不同;服务端 XML 通过组件加载时同样会进入解析路径。导出已保存宽高,不应以扩大模型、重建节点或宿主 CSS 补丁修复显示。自 `0.3.5-preview` 起,五类网关、数据对象和数据存储按实际节点容器等比缩小、居中,并只在显示层将连线端点及已有连接手柄贴合可见外轮廓;模型、标签的逻辑路径基准与原始 waypoint 均不变。完整尺寸规则见 [API:节点 DI 与视觉尺寸](API.md#节点-di-与视觉尺寸)。
|
|
4228
|
+
|
|
4229
|
+
DOM 节点内容 Renderer 仍处于 Nova 管理的标准外壳内,网关自定义内容随外壳一起缩放,角色徽标保持独立。SVG Renderer 则可接管完整节点:每个节点只调用一次,成功接管时不应用标准几何或端点校正;返回 `false` 或执行失败时沿用标准降级及既有警告规则。不要依赖内部 `node-geometry` 路径;不属于标准外轮廓的自定义图形需要由宿主自行保证其视觉与连接契约。
|
|
4230
|
+
|
|
2811
4231
|
```js
|
|
2812
4232
|
createStudioShell({
|
|
2813
4233
|
container,
|
|
@@ -2954,6 +4374,8 @@ createStudioShell({
|
|
|
2954
4374
|
|
|
2955
4375
|
## 自定义审批轨迹详情
|
|
2956
4376
|
|
|
4377
|
+
先按[审批轨迹接入指南](RUNTIME-INTEGRATION.md)完成 Runtime 数据关联,再选择详情扩展点。该指南覆盖统一点击事件、内置详情 `autoOpen`、资源 Resolver、会签和回退历史;定义态副标题 Resolver 不用于改写这些运行事实。
|
|
4378
|
+
|
|
2957
4379
|
默认 Instance Viewer 会把运行记录归一化为 `RuntimePresentation`。自定义节点通过 `runtimePresentation` 读取状态和处理人摘要,通过 `openRuntimeDetails()` 复用默认详情弹层,不需要再次实现会签与多轮审批聚合。
|
|
2958
4380
|
|
|
2959
4381
|
```js
|
|
@@ -3050,7 +4472,7 @@ createStudioShell({
|
|
|
3050
4472
|
|
|
3051
4473
|
# BPMN Nova NPM 包与发布
|
|
3052
4474
|
|
|
3053
|
-
版本:`0.3.
|
|
4475
|
+
版本:`0.3.5-preview`。BPMN Nova 只发布三个 `@bpmn-nova` 公共包,均使用 ESM、附带 TypeScript 声明、采用 Apache-2.0 License,并通过 `preview` dist-tag 发布。
|
|
3054
4476
|
|
|
3055
4477
|
## 公共包
|
|
3056
4478
|
|
|
@@ -3066,6 +4488,8 @@ Core、Model、Renderer、Runtime、Theme、Properties、Node Presentation 和
|
|
|
3066
4488
|
|
|
3067
4489
|
React/Vue 的声明入口显式重导出宿主常用的 Core 类型:`BpmnEdge`、`BpmnNode`、`EdgeType`、`ElementSelection`、`EngineId`、`LayoutOptions`、`NodeType` 和 `ProcessModel`。业务项目必须从自己选择的 Adapter 导入这些类型,不应仅为类型在 manifest 中增加 Studio 直依赖。
|
|
3068
4490
|
|
|
4491
|
+
`0.3.5-preview` 另补齐 14 个 Runtime 数据、点击事件及资源 Resolver 类型出口;只修改声明,不增加运行时代码或依赖。完整清单、旧版 Props 推导及宿主接入示例见[审批轨迹接入指南](RUNTIME-INTEGRATION.md)。这些新增出口不属于旧 `0.3.4-preview` 包。
|
|
4492
|
+
|
|
3069
4493
|
## Studio 子路径
|
|
3070
4494
|
|
|
3071
4495
|
Studio 根入口重新导出稳定的完整能力;以下子路径用于让导入意图更清晰:
|
|
@@ -3109,6 +4533,28 @@ node scripts/build-packages.mjs --package vue
|
|
|
3109
4533
|
|
|
3110
4534
|
`--package` 不接受内部 Module 名称。三个公开包之外的目录没有发布 manifest、NPM README 或独立版本生命周期。
|
|
3111
4535
|
|
|
4536
|
+
## 全局统一版本与全量发布
|
|
4537
|
+
|
|
4538
|
+
每次发布都是三个公共包的完整发布,不按源码是否改动选择包;未改动的 Adapter 也必须同步升版、构建、验证和发布。此规则不改变宿主只直接安装一个适配包的接入方式。
|
|
4539
|
+
|
|
4540
|
+
同一轮目标版本 `V` 必须满足:
|
|
4541
|
+
|
|
4542
|
+
```text
|
|
4543
|
+
根 package.json.version
|
|
4544
|
+
= @bpmn-nova/studio.version
|
|
4545
|
+
= @bpmn-nova/react.version
|
|
4546
|
+
= @bpmn-nova/vue.version
|
|
4547
|
+
= React 对 Studio 的精确依赖
|
|
4548
|
+
= Vue 对 Studio 的精确依赖
|
|
4549
|
+
= V
|
|
4550
|
+
```
|
|
4551
|
+
|
|
4552
|
+
根包为私有包,只同步版本,不发布 npm;源码内部 Module 没有独立发布版本。`--package` 仅用于单包构建或打包过程,不能用来跳过本轮某个公共包的发布。
|
|
4553
|
+
|
|
4554
|
+
发布前先查询三个包的 Registry 版本,选择统一且未被占用的目标版本,再一起更新上述版本和相关文档,执行 `npm run docs:ai` 生成 AI 文档。本地 `release:check-versions` 检查上述一致性,但不能替代发布后的远端检查。已发布版本不可覆盖;部分成功后的处理见本页“部分发布失败”。
|
|
4555
|
+
|
|
4556
|
+
只有三个包的精确目标版本均可安装、Adapter 的 Studio 依赖精确一致、三个包的目标 dist-tag 均解析到 `V`,才算本轮发布完成。当前通道为 `preview`,保持 `latest` 不变;不将“最新版”解释为更新 `latest` 标签。
|
|
4557
|
+
|
|
3112
4558
|
## 发布前验证
|
|
3113
4559
|
|
|
3114
4560
|
```bash
|
|
@@ -3140,7 +4586,7 @@ git diff --check
|
|
|
3140
4586
|
|
|
3141
4587
|
## 发布顺序
|
|
3142
4588
|
|
|
3143
|
-
|
|
4589
|
+
每轮必须发布三个包。先发布 Studio,确认 Registry 已可解析后,再发布 React 和 Vue:
|
|
3144
4590
|
|
|
3145
4591
|
```bash
|
|
3146
4592
|
(cd packages/studio && npm publish --access public --tag preview)
|
|
@@ -3150,14 +4596,20 @@ git diff --check
|
|
|
3150
4596
|
|
|
3151
4597
|
三个 manifest 都设置了 Preview 发布守卫。仍应显式使用 `--tag preview`,避免预览版本意外覆盖稳定版 `latest`。
|
|
3152
4598
|
|
|
3153
|
-
|
|
4599
|
+
发布后从仓库根目录核对;下面所有版本输出都应等于根版本,两个依赖也应精确等于该值:
|
|
3154
4600
|
|
|
3155
4601
|
```bash
|
|
3156
|
-
|
|
3157
|
-
npm view @bpmn-nova/
|
|
3158
|
-
npm view @bpmn-nova/
|
|
4602
|
+
release_version=$(node -p "require('./package.json').version")
|
|
4603
|
+
npm view "@bpmn-nova/studio@$release_version" version
|
|
4604
|
+
npm view "@bpmn-nova/react@$release_version" version dependencies --json
|
|
4605
|
+
npm view "@bpmn-nova/vue@$release_version" version dependencies --json
|
|
4606
|
+
npm view @bpmn-nova/studio@preview version
|
|
4607
|
+
npm view @bpmn-nova/react@preview version
|
|
4608
|
+
npm view @bpmn-nova/vue@preview version
|
|
3159
4609
|
```
|
|
3160
4610
|
|
|
4611
|
+
同时比较发布前后三个包的 `dist-tags.latest`,并在临时消费工程中安装本轮产物验证入口和类型。尚未通过远端核对时,报告为“部分发布”或“待验证”,不能报告全部发布成功。
|
|
4612
|
+
|
|
3161
4613
|
## Registry 清理说明
|
|
3162
4614
|
|
|
3163
4615
|
Registry 清理是不可逆的外部操作,不由构建脚本自动执行。只有在三个 `0.3.4-preview` 新产物发布、安装和子路径验证均成功后,才能在获得单独明确授权的情况下执行:
|
|
@@ -3178,14 +4630,15 @@ Registry 清理是不可逆的外部操作,不由构建脚本自动执行。
|
|
|
3178
4630
|
|
|
3179
4631
|
1. 使用 `npm view` 确认成功发布的包和版本。
|
|
3180
4632
|
2. 修复身份、网络或顺序问题。
|
|
3181
|
-
3.
|
|
3182
|
-
4. 如果已发布 tarball
|
|
4633
|
+
3. 已成功发布且内容正确的包无需重复发布;继续发布本轮尚未成功的包,直到三个包及其 `preview` 标签均对齐。这是失败恢复,不是按源码改动跳过包。
|
|
4634
|
+
4. 如果已发布 tarball 内容错误,停止本轮发布,修复后选择新的统一 Preview 版本,重新构建、验证并发布全部三个包,不能只提升一个包的版本。
|
|
3183
4635
|
|
|
3184
4636
|
## 文档
|
|
3185
4637
|
|
|
3186
4638
|
- [快速开始](GETTING-STARTED.md)
|
|
3187
4639
|
- [组件参数](COMPONENTS.md)
|
|
3188
4640
|
- [公开 API](API.md)
|
|
4641
|
+
- [审批轨迹接入指南](RUNTIME-INTEGRATION.md)
|
|
3189
4642
|
- [AI 接入指南](AI-INTEGRATION.md)
|
|
3190
4643
|
- [AI 安装执行入口](../llms.txt)
|
|
3191
4644
|
|