# 曦寒流程设计器 · 指南 共 6 页。索引见 https://flow.docs.xihanfun.com/llms.txt --- 来源:https://flow.docs.xihanfun.com/guide/designer # 设计器 `` 编辑流程定义:从物料面板拖入节点、拖动端口连线、在属性面板里改属性,问题列表随时给出校验结果;撤销重做、剪贴板、对齐分布与自动布局都是内置命令。它只改流程定义,不保存,什么时候存、存到哪里由宿主决定。完整的属性、方法、事件与命令见[设计器参考](../reference/designer)。 ## 放上页面 调用 `defineFlowDesigner()` 注册设计器,它用到的 XiHan.UI 元素与设计器零件一并注册。样式表比查看器多两份:设计器零件的 `@xihan-ui/designer-shell/style.css` 与设计器自己的 `@xihan-flow/designer/style.css`。 ```ts import { defineFlowDesigner } from '@xihan-flow/designer' import { extensions, schema } from '../schemas/approval' import '@xihan-ui/styles/index.css' import '@xihan-ui/designer-shell/style.css' import '@xihan-flow/runtime/style.css' import '@xihan-flow/designer/style.css' defineFlowDesigner() ``` 再把扩展与流程定义交给 `extensions` 与 `schema`。设计器铺满自身,宿主要给元素定高度。 ```ts const designer = document.createElement('xh-flow-designer') designer.extensions = extensions designer.paletteGroupOrder = ['human', 'control'] designer.paletteGroupLabels = xihanWorkflowZhCN.categories designer.propertyGroupOrder = ['basic', 'participants', 'form', 'execution'] designer.propertyGroupLabels = xihanWorkflowZhCN.groups designer.schema = schema ``` - `extensions` 与查看器的是同一类扩展,设计器层的扩展还能登记属性编辑器、命令与面板,见[扩展](./extensions)。同一个扩展对象换了引用会当作卸下再装上,宿主要保持引用稳定。 - 给 `schema` 赋值即装载:清空撤销历史与选择,视口按实际大小(100%)摆好——整张图在某个方向上放得下就居中,放不下就从左上角留白处起,开头的节点在眼前;画布尺寸变了照这样重摆,直到设置视口或在画布上按下指针。要看全貌用「适合窗口」(`zoomToFit()`)。赋回当前这一份不会重新装载。 - 读 `schema` 得到当前的流程定义。设计器从不改动传进来的对象,每次修改都产生一份新的定义。 ## 编辑 **新建节点**:把物料面板里的节点类型拖到画布上,在松手处新建;点条目则在视口中央新建;中央已有节点时顺着右下方错开一步,连点几次不会叠在一起,错到视口边上从左上角另起一列。物料按节点类型的类别分组,`paletteGroupOrder` 定类别的先后,`paletteGroupLabels` 给类别换显示文字。宿主也可以调用 `addNode(type, at)`。 **选择**:点节点或连线选中它,按住 Shift、Ctrl 或 ⌘ 点击则加选或取消;在空白处按住 Shift 拖出框选,同时按住 Ctrl 或 ⌘ 时追加到已有的选择上;点空白处清空选择。选择变化派发 `selection-change`,`getSelection()` 与 `select()` 读写选择。 **移动**:拖动选中的节点。`grid` 是吸附的网格边长,缺省 16,写 `false` 不吸附;设置里的「吸附对齐」开着时(缺省开,见[设置](#设置))靠近其它节点会吸附并显示对齐线。按住 Alt 松手时原节点不动,在新位置复制一份。画布有焦点时方向键微移选中的节点,按住 Shift 一次移一格网格。 **连线**:从节点的端口拖到另一个节点的端口。拖动途中预览线标出能不能连;违反节点类型的连接约束时连不上,派发 `connect-reject`,`detail` 里是两端与全部问题。选中连线后,两端的控制柄拖到别的端口即改连;中段的控制柄拖出拐点,拐点可以拖动,双击去掉;「清除拐点」把连线交回自动路由。拖动途中按 Escape 取消。 **平移与缩放**与查看器一样:在空白处拖动或滚动滚轮平移,按住 Ctrl 或 ⌘ 滚动滚轮缩放;没有选中节点时方向键平移视口。工具栏上的「拖动」(`H`)打开以后,在画布任意处按住拖动都是平移,节点上也一样,不选中、不挪节点,光标是手形;再点一下或按 Esc 退出,状态在外框的 `canvasTool` 上。小地图上按下或拖动,视口中心移到那里。 ## 属性面板 属性面板编辑选中的对象:节点按节点类型的属性描述列出字段;连线列出连线通用的属性,再加上源节点类型给出线登记的属性。选中多个对象时只编辑它们共同的属性,一次写进全部对象;什么都没选中时编辑流程定义级的属性。 字段按属性描述的分组排列。`propertyGroupOrder` 定分组的先后,`propertyGroupLabels` 给分组换标题;示例里两项都来自预设的中文文案。在同一个字段里连续输入合成一步撤销,离开字段后再改另起一步。字段的校验问题直接显示在字段上。 ## 问题列表 流程定义、扩展或校验选项变了之后,设计器重新校验,问题列表显示全部问题,同时派发 `issues-change`。错误与警告的条数在问题列表的面板头上,点一下只看或不看这一级。有问题的节点带 `data-issue`,取值是落在它上面最严重的一级(`error` 或 `warning`),皮肤据此画出标记。点一条问题,设计器选中问题所在的节点或连线并显露它;问题落在某个属性上时,焦点直接交给属性面板里的那个字段。 问题码与说明见[问题码](../reference/issues)。`validateOptions` 按问题码改写严重程度,写 `off` 即不报告: ```ts designer.validateOptions = { severity: { 'node.isolated': 'off' } } ``` `validate()` 按同样的扩展与选项立即校验一次,宿主保存前可以用它拦下有错误的定义。 ## 流程结构 流程结构面板按定义里的先后列出节点,每个节点下面是从它出去的连线,缺省收起。没有名称的节点写「种类名 · id」,有名称时副文本是种类名;连线没有名称时写「连线 · id」,副文本是「→ 去向」,连线的业务数据里有条件文字(`data.condition`,XiHan 工作流预设就这样写条件)时写成「条件 → 去向」。 - 点一行就在画布上选中它,不在视口里时移过来(节点用 `revealNode`,连线用 `revealEdge`);画布上选中的对象在面板里跟着选中,选中一条连线时展开它的出发节点。 - 多选照文件管理器:按住 Ctrl / ⌘ 点击或按 Ctrl / ⌘ + Space 加选与取消,按住 Shift 点击选一段,节点与连线可以一起选;只把新选上的第一行移进视口,只取消了几行时视口不动。Delete 删掉全部选中的。 - 指针悬停或键盘落在一行时,画布上标出那个节点或连线,离开时撤掉。 - 删除钮与 Delete 走设计器的删除;只读时不删。行不能拖动,先后跟着流程定义。 - 面板上方有搜索框,按名称与副文本过滤。标题与帮助是流程自己的说法,在设计器文案的 `outline` 里。 ## 命令 菜单栏、工具栏、右键菜单与快捷键执行的都是命令。每条内置命令都带图标,工具栏缺省只显示图标,悬停或聚焦时提示命令的名字与快捷键;撤销、复制、对齐、缩放这些四款设计器都有的命令,文案用 designer-shell 语言包里 `xh-designer-commands` 那一桶,与其它设计器一致,要改它们的文字时改那一桶;撤销、重做与清空历史本身就是 designer-shell 的共用命令,见[撤销与保存](#撤销与保存)。工具栏末组是 designer-shell 内置的「面板」:点开一张菜单,列出物料、属性、问题这些能开关的面板,勾选的显示,收起的面板从这里再打开;宿主不要哪一条,用 `commandLayout` 排掉。设置、帮助、开始引导与编辑 JSON 在菜单栏里,见下文。内置命令的 id、名称与快捷键见[内置命令](../reference/designer#内置命令);只读时会修改流程定义的命令不可用,复制、全选与缩放仍可用。命令都有对应的方法,宿主的按钮直接调用方法即可,比如 `undo()`、`duplicate()`、`align('left')`、`autoLayout({ direction: 'LR' })`;按 id 执行用 `runCommand(id)`,内置命令与扩展命令都认,命令存在且此刻可用才执行,返回是否执行了。 最上一条是菜单栏,下面是工具栏。菜单栏放通用的功能,四张菜单与四款设计器一致: - 「文件」:保存(Ctrl / ⌘ + S)、打开文件(有导入器时)、导出 PNG 与 SVG、设置(Ctrl / ⌘ + ,,同 `openSettings()`)。 - 「编辑」:撤销、重做,剪切、复制、粘贴、删除、创建副本,全选;开发者模式开着时还有编辑 JSON(Ctrl / ⌘ + Shift + J,见[编辑 JSON](#编辑-json))。 - 「视图」:放大、缩小、适合窗口、实际大小,拖动,自动布局与「对齐与分布」子菜单,「面板」与「外观」两个子菜单;外观里是工具栏、状态栏、菜单栏三个显示开关(改的是设置)。 - 「帮助」:开始引导、帮助(在新窗口打开本站的设计器一页,地址是导出的 `FLOW_DESIGNER_HELP_URL`)、快捷键(打开设置的快捷键页),关于(打开设置的关于页)。 画布有焦点时按 F10 把焦点交给菜单栏,左右键在菜单之间走,下键、Enter 或 Space 展开;再按 F10,或在菜单栏上按 Esc,回到画布。窄屏时菜单栏收成行首一颗「菜单」钮,各张菜单是它的子菜单。菜单栏常开,设置里没有关掉它的开关。 宿主自己的操作(新建、预览、返回列表)写成一个扩展的命令,`placement` 写 `menubar:file` 就进「文件」菜单,写了 `toolbar` 也上工具栏;模板标题写成设计器的子元素 `slot="menubar-center"`,显示在整条菜单栏的正中,放不下时截断加省略号,再窄就藏起;保存状态写成 `slot="status-end"`,显示在状态栏右端、缩放比例的左边,状态栏在设置里关掉时跟着不显示;别的要常显的内容写 `slot="menubar-end"`,显示在菜单栏右边。这样设计器外面不必另起一条操作条。 ```ts designer.extensions = [{ id: 'acme.host', commands: [ { id: 'acme.new', label: '新建', shortcut: ['Mod', 'Alt', 'N'], placement: ['menubar:file'], run: () => location.assign('/flows/new') }, { id: 'acme.back', label: '返回列表', placement: ['menubar:file', 'toolbar'], run: () => history.back() }, ], }] const title = document.createElement('span') title.slot = 'menubar-center' title.textContent = '请假审批 · LEAVE-01' const saved = document.createElement('span') saved.slot = 'status-end' saved.textContent = '已保存' designer.append(title, saved) ``` 右键菜单按落点给出不同的命令:节点上是剪切、复制、创建副本与删除,选中两个以上节点时加上对齐,三个以上时再加上分布;连线上是删除与清除拐点;空白处是粘贴、全选、自动布局与适合窗口,开发者模式开着时末尾是编辑 JSON。在画布上按 ContextMenu 键或 Shift + F10,按当前的选择打开菜单。右键事件可以取消,取消后内置菜单与浏览器的菜单都不出现,宿主可以在 `detail` 的 `x`、`y` 处弹自己的菜单。 选中节点时,选中框上方浮着一条选中工具条:创建副本、复制与删除,选中几个节点时前面多一个「对齐与分布」菜单(`align-menu`,点开列出六种对齐与两种分布)。贴顶放不下时翻到下方,拖动途中收起;只选了连线、没有选中或只读时不出现。画布有焦点时按 Alt + F10 把焦点交给它,在工具条里按 Esc 回到画布。 `commandLayout` 重排工具栏、菜单栏的各张菜单、各右键菜单与选中工具条,键是位置(`toolbar`,菜单栏是 `menubar:file`、`menubar:edit`、`menubar:view`、`menubar:help`,右键菜单是 `menu:node`、`menu:edge`、`menu:blank`,选中工具条是 `selection:node` 与 `selection:nodes`),值是若干组命令 id;某张菜单或某个选中工具条排空了就不出现。写了某个位置就只显示列出的命令,没写的位置用内置排列;排列里写了不存在的命令 id 会抛 TypeError,不会静默跳过。 ```ts designer.commandLayout = { 'toolbar': [['undo', 'redo'], ['auto-layout'], ['zoom-to-fit', 'zoom-to-actual']], 'menu:blank': [['paste'], ['auto-layout']], } ``` 面板是物料(`palette`)、流程结构(`outline`)、属性(`properties`)、问题(`issues`)、历史记录(`history`)五张卡片与画布右下角的小地图(`minimap`),id 在包入口导出的 `FLOW_PANELS` 里。流程结构停在左栏物料下面;历史记录缺省隐藏,打开后停在右栏属性下面。`defaultLayout` 是宿主给的缺省布局:用户没存过布局时用它,「恢复默认布局」也回到它;不给时全部显示、各在缺省的位置。形状是 `@xihan-ui/designer-shell` 的 `DesignerLayout`,写了的字段整项覆盖缺省,常用的是 `hidden`(收起哪些面板)与 `docks`(各栏放哪些面板、谁先谁后)。 ```ts designer.defaultLayout = { version: 1, hidden: [FLOW_PANELS.palette, FLOW_PANELS.issues, FLOW_PANELS.minimap] } ``` 底部的状态栏左边写选中的对象:什么都没选中时是流程里有几个节点、几条连线,选中一个节点时是它的类型、位置与尺寸,选中一条连线时是连线名,选中多个时是选了几个;右边是缩放比例,点一下回到 100%。设置里关掉「状态栏」时整条不显示。 物料、属性与问题列表各是一张卡片:面板头写标题,属性面板的副标题是当前编辑的对象,「?」弹出这块面板的说明,收起钮把它收成标题条(双击面板头也行),「更多」菜单里换栏、调序、浮出,「×」隐藏它;按住面板头可以拖到左、右、下另一栏,拖到画布上就浮出成小窗。工具栏与「视图」菜单里的「面板」逐块勾选显隐,另有显示或隐藏所有面板(Ctrl / ⌘ + \\)、还原面板显隐(Ctrl / ⌘ + Shift + \\)与恢复默认布局;每块面板还有一条开关命令 `panel:<面板 id>`,可以排进 `commandLayout`。 布局改了,整份写进设置的 `layout.panels`(`DESIGNER_SETTING_KEYS.layout`),和设置对话框里改一条一样派发 `settings-change`,宿主存下来、下次随 `settings` 赋回即可;给了 `settingsStorageKey` 时设计器自己存。宿主要固定布局,用 `settingsPolicy`:`layout.panels` 锁住整份,`layout.panels.<面板 id>` 藏掉或锁定一块面板。 除了命令的快捷键,画布有焦点时还认这些键:Backspace 删除,`=` 放大,F10 进出菜单栏;设置的「快捷键」页把命令的快捷键与这些键列成一张表。Ctrl / ⌘ + C、X、V 走系统剪贴板:复制出的是一段 JSON 文本,能在同一页面、别的页面或别的标签页的设计器里粘贴;读不出流程内容的文本交给扩展的导入器(见[导入与导出](./extensions#导入与导出)),没有导入器接下时不会贴进来。宿主调 `paste(内容)` 时每次比上一次多错开一格;给了落点 `paste(内容, { at })` 时内容整体的中心放到 `at`(世界坐标,视口中央是 `getViewportCenter()`),左上角吸附网格,不累加错开。剪贴板的文本格式由 `encodeFlowClipboard` 与 `decodeFlowClipboard` 互转。 ## 设置 设置是用户偏好,每人每机一份,不随流程定义保存。`openSettings(section?)` 打开设置对话框,左边一个分区一页: - 「基础」(`general`):主题(跟随页面、跟随系统、亮色、暗色)、主题色(跟随页面、八种预设、自定义颜色)、状态栏、自动保存、开发者模式,以及分页与渲染两条调试日志(开发者模式开着才能改)。 - 「语言」(`language`):跟随页面(缺省)、跟随系统,或在十种语言里选一种,名字是各自的本名(简体中文、繁體中文、English、日本語……)。 - 「布局」(`layout`):一张面板表,逐块面板改显隐、位置、先后与收起,表下一颗「恢复默认布局」。 - 「工具栏」(`toolbar`):按钮的显示方式(图标、文字、图标和文字,缺省只显示图标)、显示工具栏开关,以及一张按钮表:按缺省排列列出工具栏上的命令,可以勾掉不要的、拖动换先后、跨组挪,表下「恢复默认」。工具栏的这些只走设置,没有单独的属性;用户的改动压在宿主的 `commandLayout` 之上。 - 「画布」(`flow`):自动布局的方向与两种间距、吸附对齐。 - 扩展登记的分区,排在后面。 - 「帮助」「快捷键」「关于」三页在最后:帮助页是几段操作说明与「开始引导」「帮助」两颗钮;快捷键页把命令的快捷键与画布认的几个键列成一张表,「通用」里方向键一行写明没有选中节点时平移画布,Esc 一行写明取消正在进行的拖动;画布在空白处直接拖动就平移,所以没有「按住空格拖动」那一行;关于页写设计器的版本(包入口导出的 `FLOW_VERSION`)与许可证,扩展可以在后面加几行(扩展的 `about` 字段)。 分区 id 与 `flow`、内建的 `general`、`language`、`layout`、`toolbar`、`help`、`shortcuts`、`about` 或别的扩展重名时,给 `extensions` 赋值就抛 `DesignerExtensionError`。 ```ts export function openCanvasSettings(designer: XhFlowDesigner): void { designer.openSettings(FLOW_SETTINGS_SECTION) } ``` 值是扁平的键值,键是「分区 id.条目 id」,如 `general.theme`、`flow.snap`,没写的键取缺省值。宿主给缺省、锁定条目或读值时用常量,不手写键:「画布」分区的 id 与各条目的键是包入口导出的 `FLOW_SETTINGS_SECTION` 与 `FLOW_SETTING_KEYS`,「基础」「语言」「布局」「工具栏」分区的键是 `@xihan-ui/designer-shell` 的 `DESIGNER_SETTING_KEYS`(如 `general.brand`、`language.locale`、`toolbar.display`、`toolbar.layout`);以后条目改了 path,常量跟着变,宿主的缺省不会静默失效。`settings` 赋值即生效;用户在对话框里改了一条,设计器换成新的一份并派发 `settings-change`,宿主存起来、下次赋回。`settingsPolicy` 隐藏或锁定条目,键是条目键或分区 id:`hidden` 不显示,一个分区的条目全藏了就没有这一页;`locked` 只读,说明换成「由所在系统管理」。 ```ts export function wireSettings(designer: XhFlowDesigner, saved: FlowSettings, store: (settings: FlowSettings) => void): void { // 宿主自己的缺省压在下面,用户存过的值盖在上面 designer.settings = { [FLOW_SETTING_KEYS.layoutDirection]: 'LR', ...saved } designer.settingsPolicy = { [DESIGNER_SETTING_KEYS.theme]: 'hidden', [DESIGNER_SETTING_KEYS.brand]: 'hidden', [FLOW_SETTING_KEYS.snap]: 'locked' } designer.addEventListener('settings-change', (event) => { store((event as CustomEvent<{ settings: FlowSettings }>).detail.settings) }) } ``` - **主题只作用于设计器**:主题与主题色写在 `` 自己的 `data-theme`、`data-brand` 上,页面其余部分不变;设置对话框、菜单这些浮层跟着设计器变。主题色是内置的:跟随页面(缺省),或 UI 的八种预设,或自定义颜色,宿主不用传预设。 - **语言只作用于设计器**:设置里选了语言,`` 把这一语言的 UI、designer-shell 与流程语言包作为自己的 `xhConfig` 交出去,设计器、里面的元素与它弹出的浮层都换成这种语言(日期、分页这些 UI 文案也一样),页面别处不变。跟随系统按浏览器的语言偏好挑最接近的一种,挑不到用英文;语言包第一次用到时才加载。跟随页面时 `xhConfig` 是空对象,照页面的全局配置。 - **画布的设置**:`autoLayout()` 没给的方向与间距、工具栏的「自动布局」都取「画布」分区的值;吸附对齐关掉时拖动只吸附网格。小地图是一块面板,显隐跟着布局走。 - **扩展读设置**:命令、面板与扩展从上下文的 `settings` 读值,各分区没写的条目已经取了缺省值,如 `context.settings['flow.snap']`。 - **自动保存**:设置里开着自动保存时,有未保存的修改,停手约一秒就把整份流程定义存成这台浏览器里的草稿(localStorage),宿主不参与;设计器变干净(宿主存完调 `markSaved()`,或撤销回到保存点)时删掉草稿,关掉自动保存不删已有的草稿。宿主给 `draftKey`(同名 attribute `draft-key`,通常是流程 id)区分不同流程,不给按页面地址区分。下次宿主装进同一个键的流程、和草稿不一样时,问「发现未保存的草稿(时间)」,恢复会整份换上草稿、记一步撤销,丢弃即删掉草稿。不要这一项时用 `settingsPolicy` 把 `general.autoSave` 藏起或锁住。Ctrl / ⌘ + S 照旧派发 `save`,与自动保存无关。 - **调试日志**:开发者模式与「渲染调试日志」都开着时,每次重画在浏览器控制台记一组「[xh-flow-designer] render · update: 改动的属性 耗时」,导出图片也记一组;流程没有分页,「分页调试日志」开了也没有日志。 独立使用、没有宿主替你存时,给 `settingsStorageKey`:连上文档时若没赋 `settings` 就从根所在窗口的 localStorage 读,用户改了设置就写回去;标了 `secret` 的文本条目(如密钥)不存。读写失败派发 `settings-storage-error`,设置留在内存里。不给就不读写存储。 ```ts export function standalone(designer: XhFlowDesigner): void { designer.settingsStorageKey = 'flow-designer:settings' designer.addEventListener('settings-storage-error', (event) => { console.warn('设置没能读写本机存储,这次改的只在内存里', (event as CustomEvent<{ error: unknown }>).detail.error) }) } ``` ## 新手引导 设计器第一次显示出来、画布铺好之后,自动开始一次新手引导:先讲菜单栏(各张菜单里有什么,F10 进出菜单栏),再一步步高亮工具栏、节点面板、画布、属性面板、问题面板与状态栏,接着是流程自己的三步——连线(指向第一个端口)、自动布局与导出图片(指向工具栏上的钮,并说它们也在「视图」「文件」菜单里),最后一步是设置:高亮「文件」菜单,设置在那里,引导能从「帮助」菜单重新开始。目标看不见的那步自动跳过,比如画布上还没有节点时不讲连线。 窄屏时菜单栏收成一颗「菜单」钮,讲菜单栏与设置的两步都落在这颗钮上。 - 走完、跳过或关上之后,设置里记下 `help.tourDone`(`@xihan-ui/designer-shell` 的 `DESIGNER_TOUR_DONE_KEY`)并派发 `settings-change`,以后不再自动开始。宿主替用户存设置时,这个键随设置一起存。 - 设计器藏着时不开始,等它显示出来再看。宿主不要自动开始时写 `autoTour = false`(同名 attribute `auto-tour="false"`)。 - 引导限在设计器的外框里:遮罩只盖外框,高亮框裁在外框里,页面别处照常可读可点。同一页同时只有一个引导:别的设计器的引导开着时不自动开始。 - `startTour()` 随时从头开始,同一页里别的引导开着时先收起它,没有能走的步骤时返回 `false`;「帮助」菜单与设置帮助页里的「开始引导」、`start-tour` 命令做的是同一件事。 - 扩展可以登记自己的步骤,见[扩展](./extensions)的 `tourSteps`。 ## 撤销与保存 每次修改记为一步撤销,拖动这类连续操作松手时才记为一步。`canUndo`、`canRedo` 表示能不能撤销重做,`undo()`、`redo()` 执行。 - **每一步带说明**:如「移动 任务 approve_1」「添加 3 个节点」「修改「名称」」「自动布局」「编辑 JSON」。单个节点写「种类 名称」(没有名称时是 id),几个节点写个数;创建副本与按住 Alt 拖动复制都写「复制」加节点;说明跟着界面语言变。撤销、重做钮的名字带上下一步的说明,如「撤销:移动 任务 approve_1」,按钮上看得见的字仍是「撤销」。 - **合并**:在同一个属性字段里连续输入合成一步;画布有焦点时按方向键微移,同一组选中的节点一秒内连按合成一步,换了选中另起一步。 - **快捷键**:撤销 Ctrl / ⌘ + Z,重做 Ctrl / ⌘ + Shift + Z,另认 Ctrl + Y。 - **清空历史**:命令 `clear-history`,不上工具栏,在历史记录面板的面板头上;点了先弹确认框,确定后才清空,清空后不能再撤销到之前。 - **历史记录面板**:列出每一步与起点,选一步(或起点)就一次跳过去,之后的步骤留着、可以再重做回来;只读时只能看。撤销、重做、清空与跳转之后,读屏会礼貌地读一句。宿主自己改流程定义时用 `execute(operations)`,一组操作记为一步,操作的写法见[流程定义结构](./schema#编辑操作)。 流程定义每次变化派发 `schema-change`,`detail` 里是新的定义、这次的操作与种类:`execute` 是编辑,`undo`、`redo` 是撤销重做,`load` 是装载了新定义。 ```ts designer.addEventListener('schema-change', (event) => { const { kind, operations } = (event as FlowDesignerEventMap['schema-change']).detail status.textContent = `${KIND_TEXT[kind]}:${operations.map(operation => operation.type).join('、')}` }) ``` `dirty` 表示有没有未保存的修改,变化时派发 `dirty-change`。设计器不自己保存:画布有焦点时按 Ctrl / ⌘ + S,或点「文件」菜单的「保存」(命令 `save`),只派发 `save`,宿主保存成功后调用 `markSaved()` 把当前状态记为已保存。撤销回已保存的那一步时 `dirty` 也会回到 `false`。 ```ts const saveNow = (): void => { // 宿主在这里把 designer.schema 交给后端,保存成功后记为已保存 designer.markSaved() status.textContent = '已保存' } const save = button('保存', saveNow) save.disabled = true designer.addEventListener('save', saveNow) designer.addEventListener('dirty-change', (event) => { save.disabled = !(event as FlowDesignerEventMap['dirty-change']).detail.dirty }) ``` ## 编辑 JSON `openJsonEditor()` 打开一个编辑整份流程定义 JSON 的对话框(也是 `edit-json` 命令做的事)。`edit-json` 命令只在设置里开着开发者模式时存在:关着时菜单栏、右键菜单里都没有它,快捷键也不响应;宿主要给用户这个入口,就在 `settings` 里预先写 `general.developer: true`,或用 `settingsPolicy` 锁住它。`openJsonEditor()` 方法照样能调。对话框:编辑框里写出当前的定义,边改边检查,下面列出问题,底边是格式化、复制、取消与应用。 - 问题与问题列表同一套:先按读入检查结构,读得了再按当前的扩展与 `validateOptions` 校验,说明与所在对象的名字与问题列表一致。点一条,光标跳到那个路径在文字里的位置。 - 文字解析不了、有错误级的问题或与打开时相同时,「应用」不可用。 - 应用即整份换上,记为一步撤销,派发 `schema-change`(种类是 `execute`),选择清空,视口随后按实际大小摆好;撤销一步回到原来那份。 - 只读时能看、能格式化与复制,不能应用。 ## 导出图片 `export("svg" | "png", options)` 与查看器的导出图片相同,选项也一样;选中的节点与连线按没选中的样子画。它是设计器自带的图片导出器,与扩展登记的导出器走同一个 `export()`。 工具栏最后一组是「导出 PNG」与「导出 SVG」,流程图是空的时不可用,只读时照样能用。导出后派发可以取消的 `export` 事件,`detail` 里是格式与文件;没被取消时存成 `flow.png` 或 `flow.svg` 下载,宿主想自己命名或上传就取消它,拿 `detail.blob` 自己存。不想要这两个按钮时用 `commandLayout` 把它们去掉。 ## 只读 `readonly` 打开后仍可以选择、浏览、复制与缩放,不能修改:拖动、连线、删除、粘贴与属性面板的编辑都不起作用,会修改的方法什么都不做。元素上带 `data-readonly`。 同名 attribute 是三态的,与 XiHan.UI 的元素一致:不写为缺省(不只读);写 `readonly="false"` 为 `false`;写 `readonly`、`readonly="true"` 或别的值为 `true`;摘掉 attribute 回到缺省。这是行为变化:以前只要写了这个 attribute 就算只读,`readonly="false"` 也不例外。经 property 赋值不受影响。 ```ts const readonly = button('只读', () => { designer.readonly = !designer.readonly readonly.setAttribute('aria-pressed', String(designer.readonly)) }) readonly.setAttribute('aria-pressed', 'false') ``` ## 文案 设计器的缺省文案是英文。全站换语言时与查看器一样合并同一语言的流程语言包(如 `flowZhCN`、`flowJaJP`),写法见[快速开始](../quickstart#中文界面)。单个实例要改时写 `translations`,它压在全局配置之上;可以改的文案见 [FlowDesignerTranslations](../reference/designer#flowdesignertranslations)。 --- 来源:https://flow.docs.xihanfun.com/guide/extensions # 扩展 节点类型、属性、编辑方式、规则、节点渲染、属性编辑器、命令与面板,都经扩展登记。扩展是普通对象,按实例交给元素的 `extensions`,没有全局注册:同一页面上的两个设计器可以装不同的扩展。完整的字段见[扩展参考](../reference/extensions)。 ## 三层 | 层 | 类型 | 所在的包 | 在下一层之上加了什么 | 谁读 | | --- | --- | --- | --- | --- | | core | `FlowCoreExtension` | `@xihan-flow/core` | 节点类型、连线与流程定义级属性、编辑方式、规则、问题说明 | 校验、编辑操作、查看器、设计器 | | 运行时 | `FlowRuntimeExtension` | `@xihan-flow/runtime` | 节点内容的渲染 `nodeRenderers` | 查看器、设计器 | | 设计器 | `FlowDesignerExtension` | `@xihan-flow/designer` | 属性编辑器、命令、面板、`setup` | 设计器 | 下层的扩展对象可以直接交给上层的元素,core 层的扩展同时交给查看器、设计器与 `validateDocument`。服务端或 Node.js 里只装 core 层,校验结论与设计器一致。 - `id` 全局唯一,用包名加功能名,如 `@example/leave`。它也是节点、连线与流程定义上扩展数据的键,见[业务数据与扩展数据](./schema#业务数据与扩展数据)。 - 内置扩展排在最前,传入的按数组顺序排在后面;物料、属性与命令都按这个顺序。 - 同一个扩展对象换了引用会当作卸下再装上,宿主要保持扩展数组与其中对象的引用稳定。 下面用一个请假流程走一遍:core 层登记四种节点、一种编辑方式与一条规则,设计器层再加编辑器、命令、面板与 `setup`。 ## 节点类型 ```ts const nodeTypes: readonly FlowNodeTypeDefinition[] = [ { type: 'apply', label: '发起', category: 'leave', size: { width: 120, height: 40 }, ports: 'out', entry: true, maxCount: 1, }, { type: 'approve', label: '审批', category: 'leave', size: { width: 160, height: 48 }, ports: 'both', defaults: { label: '审批' }, properties: [ { path: ['label'], control: { kind: 'text' }, label: '名称', group: 'basic', required: true }, { path: ['data', 'approver'], control: { kind: 'leave.person' }, label: '审批人', group: 'basic', required: true }, { path: ['data', 'days'], control: { kind: 'number', integer: true, min: 1 }, label: '超过几天才审批', group: 'rule' }, ], outgoingEdges: [ { path: ['data', 'result'], control: { kind: 'select', options: [{ value: 'agree', label: '同意' }, { value: 'reject', label: '驳回' }] }, label: '审批结果', }, ], }, { type: 'notify', label: '抄送', category: 'leave', size: { width: 140, height: 40 }, ports: 'both', properties: [ { path: ['label'], control: { kind: 'text' }, label: '名称', group: 'basic' }, { path: ['data', 'to'], control: { kind: 'tags' }, label: '抄送给', group: 'basic' }, ], }, { type: 'archive', label: '归档', category: 'leave', size: { width: 120, height: 40 }, ports: 'in', terminal: true, }, ] ``` - `type` 是类型名,节点的 `type` 写它;`label` 是显示名,物料面板、属性面板与问题说明都用它。`icon` 是 XiHan.UI 的图标记录(如 `@xihan-ui/icons` 里的图标),画布上的节点与物料条目都把它画在文字前面。 - `category` 是物料面板里的类别,设计器的 `paletteGroupOrder` 与 `paletteGroupLabels` 按类别排序、换标题。 - `size` 是新建节点的尺寸,`defaults` 是新建时写入的标签与 `data`。 - `ports` 写预设或具名端口。预设 `in` 是左侧一个入口,`out` 是右侧一个出口,`both` 两者都有,`none` 没有端口;具名端口写 `id`、方向(`in`、`out`、`both`)、所在的边与沿边的相对位置,还可以限制连线条数;写了 `label` 的端口,设计器把名字写在端口外侧,随端口一起显出。连线的端点不写端口时,取方向相符的第一个端口。 - `entry` 的节点是可达性分析的起点,不要求有入线;`terminal` 的节点不要求有出线。`minCount`、`maxCount` 限制这类节点的个数。 - `connection` 是连接约束:入线、出线的最多条数,能连向、能接受的节点类型,以及是否允许连向自己。设计器拖动连线时按它判断能不能连,校验也按它报问题。 ## 属性 `properties` 是属性面板里列出的属性,`path` 相对节点本身:`['label']` 是标签,`['data', …]` 写进节点的业务数据。每个属性写编辑方式 `control`,内建的有 `text`、`number`、`boolean`、`select`、`tags`、`color`、`json` 等,全部种类与取值约束见 XiHan.UI 的 [designer-kit](https://ui.docs.xihanfun.com/runtime/designer-kit)。 - `group` 是属性面板里的分组,设计器的 `propertyGroupOrder` 与 `propertyGroupLabels` 按分组排序、换标题;`label` 是字段名。 - `required` 的属性必须填写;`default` 是新建节点或连线时写入的缺省值;`visibleWhen` 按别的属性的值决定显不显示。 - 校验按属性描述检查取值:必填没填、类型不对、超出范围、不在选项里等,问题码见[问题码](../reference/issues)。 连线的属性有两处:节点类型的 `outgoingEdges` 只加在从这类节点连出的连线上,如审批的结果;扩展的 `edgeProperties` 加在所有连线上。连线的标签与虚线是内置的连线属性。节点类型的 `outgoingEdgeStyles` 按连线数据给从这类节点连出的连线外观,如 `{ when: { path: ['data', 'isDefault'], equals: true }, style: { dashed: true } }` 把默认分支画成虚线;`when` 的写法同属性描述的 `visibleWhen`,路径相对连线本身。连线自己写的外观压过推出的,推出的外观只用于绘制,不写回流程定义。扩展的 `documentProperties` 是流程定义级的属性,什么都没选中时属性面板编辑它们。 ## 编辑方式 内建的编辑方式不够用时,在 `controls` 里登记新的种类,键就是属性描述里的 `control.kind`,不能与内建的重名。`check` 检查已经填写的取值,返回问题码表示不合格: ```ts /** 审批人这种编辑方式:取值要是名单里的一位。 */ const controls: FlowCoreExtension['controls'] = { 'leave.person': { check: value => (typeof value === 'string' && PEOPLE.includes(value) ? undefined : 'invalid-option') }, } ``` core 层只管取值对不对;属性面板里用什么控件编辑它,由设计器层的扩展按种类提供编辑器,见下文的[属性编辑器](#属性编辑器)。没有登记的种类在校验时报 `unknown-control`。 ## 规则 规则检查节点类型与属性描述表达不了的约束,拿到流程定义与合并后的节点类型,返回问题: ```ts const rules: FlowCoreExtension['rules'] = [ document => document.nodes.flatMap((node, index) => { if (node.type !== 'approve') return [] const rejectable = document.edges.some(edge => edge.source.node === node.id && edge.data?.result === 'reject') return rejectable ? [] : [{ code: 'leave.no-reject', path: ['nodes', index], target: { kind: 'node', id: node.id }, severity: 'warning' as const }] }), ] ``` - 问题码必须以扩展 id 的末段加点开头,`@example/leave` 的规则写 `leave.` 开头的码,不符时校验当场抛 `TypeError`。 - `path` 从流程定义的根算起;带上 `target` 时,设计器在问题列表里点这条问题会选中并显露那个对象,节点上也会标出问题。 - `severity` 不写按错误处理。宿主可以用 `validateOptions.severity` 按问题码改写,扩展的问题码也认。 问题的说明由扩展的 `issueText` 给出,返回 `undefined` 时交给设计器的文案: ```ts export const leave: FlowCoreExtension = { id: '@example/leave', nodeTypes, controls, rules, issueText: issue => (issue.code === 'leave.no-reject' ? '审批节点要连出一条驳回' : undefined), } ``` ## 设计器层 设计器层的扩展可以与 core 层的分开写,一起放进 `extensions`。下面的示例里,审批人用下拉框选,工具栏多了「加一个审批」与「吸附网格」,右侧多了一块统计,双击节点时连同它的出线一起选中: ```ts const designer = document.createElement('xh-flow-designer') designer.extensions = [leave, leaveTools] designer.paletteGroupLabels = { leave: '请假' } designer.propertyGroupOrder = ['basic', 'rule'] designer.propertyGroupLabels = { basic: '基本', rule: '规则' } designer.schema = schema ``` ### 属性编辑器 编辑器是一个函数,拿到上下文,返回放进属性面板的元素,值或只读状态变化时设计器调用 `update`: ```ts /** 审批人用原生下拉框选。 */ const personEditor: FlowPropertyEditor = (initial) => { const select = document.createElement('select') select.dataset.xhPart = 'control' select.setAttribute('aria-label', initial.descriptor.label ?? '') select.append(new Option('(未选)', ''), ...PEOPLE.map(name => new Option(name, name))) let context = initial select.addEventListener('change', () => { context.setValue(select.value === '' ? undefined : select.value) context.commit() }) const update = (next: typeof initial): void => { context = next select.value = typeof next.value === 'string' ? next.value : '' select.disabled = next.readonly } update(initial) return { element: select, update } } ``` - 上下文里有当前值、属性描述、全部被编辑的对象与是否只读。`setValue` 写入新值,`undefined` 表示清除;`commit` 表示一次编辑结束,之后的写入另起一步撤销。 - `editors` 有三张表:`byControl` 按编辑方式的种类,`byPath` 按属性路径,`byTarget` 按对象种类再按属性路径。对象种类是节点类型,连线是 `edge`,流程定义是 `document`;路径键是 `formatJsonPath` 的写法,如 `data.approver`。 - 查找时先看 `byTarget`,再看 `byPath`,最后看 `byControl`;后面的扩展压过前面的。 - 设计器内建的编辑器(如 XiHan 工作流预设的[启动变量](./xihan-workflow#启动变量))垫在所有扩展之下,扩展在同一张表里登记同一个键就换掉它。 ### 命令 ```ts const commands: FlowDesignerExtension['commands'] = [ { id: '@example/leave-tools.add-approve', label: '加一个审批', placement: ['toolbar', 'menu:blank'], enabled: ({ designer }) => !designer.readonly, run: ({ designer }) => designer.addNode('approve'), }, { id: '@example/leave-tools.grid', label: '吸附网格', placement: ['toolbar'], checked: ({ designer }) => designer.grid !== false, run: ({ designer }) => { designer.grid = designer.grid === false ? 16 : false }, }, ] ``` - `id` 用扩展 id 加点加名字。`placement` 写缺省出现在哪里:`toolbar`,菜单栏的 `menubar:file`、`menubar:edit`、`menubar:view`、`menubar:help`,右键菜单的 `menu:node`、`menu:edge`、`menu:blank`,选中工具条的 `selection:node`、`selection:nodes`;扩展命令排在内置命令之后,自成一组(在菜单栏里排在那张菜单收尾的一组之前);不写时只能经快捷键或 `commandLayout` 出现。扩展还能用 `menubarMenus` 往菜单栏上加自己的菜单 `{ id, label, order? }`(`order` 写在 30–80 之间,排在「视图」与「帮助」之间),命令写 `menubar:<菜单 id>` 落进去。 - `enabled` 不写时总是可用;写了 `checked` 就是开关式命令,按钮与菜单项显示选中状态;`tone` 写 `danger` 时按危险操作的颜色显示。 - `shortcut` 写成 `['Mod', 'Shift', 'P']` 这样的键序列,`Mod` 在 macOS 上是 ⌘,其它系统是 Ctrl,画布有焦点时生效。 - `run` 与其它回调拿到的上下文里有设计器本身与此刻的文案。 ### 面板 ```ts const panels: FlowDesignerExtension['panels'] = [ { region: { name: 'leave-summary', position: 'end', title: '统计' }, create: (context) => { const element = document.createElement('p') const update = ({ designer }: typeof context): void => { const approvals = designer.schema.nodes.filter(node => node.type === 'approve').length element.textContent = `共 ${designer.schema.nodes.length} 个节点,其中审批 ${approvals} 个` } update(context) return { element, update } }, }, ] ``` - `region` 是面板占的区:`name` 不能与外框的区(`menubar`、`toolbar`、`canvas`、`status`)、内置面板(`FLOW_PANELS` 里的物料、流程结构、属性、问题、历史记录与小地图)或别的扩展的重名;`position` 是 `start`、`end`、`bottom` 之一,即左栏、右栏、下栏,同一栏的面板上下排列。 - 写了 `title` 才画成卡片:面板头写标题,带收起、「更多」(换栏、调序、浮出、收成标题条、隐藏)与关闭钮,按住面板头能拖到别处;`icon` 用在工具栏「面板」菜单与浮出小窗的标题栏,`help` 是面板头的说明,`defaultHidden` 缺省不显示。卡片的做法见 XiHan.UI 的[面板卡片](https://ui.docs.xihanfun.com/runtime/designer-shell#面板卡片)。 - `create` 在给 `extensions` 赋值、装上这个扩展时调用一次;之后设计器每次更新都调用 `update`,扩展被换下前调用 `destroy`。 - 扩展面板与内置面板一样进布局:能换栏、浮出、收起与隐藏,记在设置的 `layout.panels` 里;每块各有一条开关命令 `panel:<区名>`。 ### 物料来源 `paletteSources` 往物料面板里加几组条目,接在节点类型的分组后面,适合放常用片段、行业模板这类「一点就放下一段」的物料: ```ts export const snippets: FlowDesignerExtension = { id: '@acme/flow-snippets', paletteSources: [{ id: '@acme/flow-snippets.common', groups: () => [{ id: '@acme/flow-snippets.common', label: '常用片段', items: [{ id: '@acme/flow-snippets.two-level-approval', label: '两级审批', description: '部门审批后转财务审批' }], }], insert: (_item, { designer }, at) => { designer.paste(twoLevelApproval, { at: at ?? designer.getViewportCenter() }) }, }], } ``` - `groups` 在设计器每次更新时现取,返回几组物料;条目的写法同 designer-shell 的物料面板(`label`、`icon`、`description`、`disabled`、`children` 等),内容与上一次一样时面板不重排。组 id 与条目 id(含下级)在整个物料面板里不能重名,节点类型的条目 id 就是类型名;重名时抛 TypeError。 - 条目被点按或回车时调用 `insert`,不带落点;拖进画布松手时带上松手处的世界坐标。放什么、放在哪由来源自己定,常用的是 `designer.paste(片段, { at })`:片段整体的中心放到 `at`,左上角吸附网格;点按时没有落点,取 `getViewportCenter()` 放在视口中央。 - `order` 排各来源的先后,不写按 100。只读时物料面板的条目一概不能用。 ### 导入与导出 `importers` 把粘贴、拖进画布的外部内容与打开的文件换成流程内容,`exporters` 把当前流程定义换成文件: ```ts export const jsonFiles: FlowDesignerExtension = { id: '@example/json-files', importers: [{ id: '@example/json-files.open', fileTypes: ['.json', 'application/json'], accepts: source => source.file !== undefined && (source.mime === 'application/json' || source.name?.endsWith('.json') === true), import: async (source) => { const read = readDocument(JSON.parse(await source.file!.text())) if (!read.ok) throw new TypeError('这个文件不是流程定义') return { document: read.document } }, }], exporters: [{ id: '@example/json-files.save', label: '流程定义(JSON)', formats: ['json'], export: async (_format, { designer }) => JSON.stringify(designer.schema, null, 2), }], } ``` - 来源有三处:粘贴(`kind` 为 `clipboard`)、拖进画布(`drop`)与 `openFile()` 选的文件(`file`)。粘贴与拖放先列文件,再按链接、HTML、纯文本列出文本;来源按先后、导入器按扩展顺序问 `accepts`,第一个接下的导入。设计器自己复制出的内容直接粘贴,不经过导入器。 - `import` 可以是异步的,返回 `{ nodes, edges }` 片段或 `{ document }` 整份流程定义,返回 `undefined` 时什么都不放。片段整体挪到落点(粘贴与打开文件时是视口中央,拖放时是指针处),左上角吸附网格,按粘贴的规则换新 id、选中并记一步撤销;整份定义换掉当前定义,同样记一步撤销,视口随后适配。 - `import` 抛错,或结果的结构不合法、id 重复、用了没登记的节点类型时,派发 `import-error`,`detail` 里是导入器的 id、来源与错误,什么都不放;打开的文件没有导入器接下时也派发,此时没有导入器的 id。 - 装了导入器后,工具栏最前面多出「打开文件」;宿主也可以自己调 `openFile()`。只读时导入一概不接。 - `fileTypes` 写这个导入器认的文件类型(`.json` 这样的扩展名或 MIME),「打开文件」的文件框按各导入器写的合起来筛选;都没写时不限类型。它只管文件框里列哪些文件,接不接仍由 `accepts` 定。 - `export(format, options)` 交给登记了这种格式的第一个导出器,`options` 原样放进上下文;得到的文件内容或文本存到哪里由宿主定。没有导出器认这种格式时返回被拒的 Promise(`RangeError`)。设计器自带一个图片导出器,认 `svg` 与 `png`,排在扩展的导出器之前(见[设计器的导出图片](./designer#导出图片))。 ```ts export function wire(designer: HTMLElementTagNameMap['xh-flow-designer']): void { designer.extensions = [jsonFiles] designer.addEventListener('import-error', (event) => { const { importer, error } = (event as CustomEvent<{ importer?: string, error: unknown }>).detail console.warn(importer ?? '没有导入器接下这个文件', error) }) } export async function save(designer: HTMLElementTagNameMap['xh-flow-designer']): Promise { const text = await designer.export('json') const url = URL.createObjectURL(new Blob([text], { type: 'application/json' })) const link = Object.assign(document.createElement('a'), { href: url, download: 'flow.json' }) link.click() URL.revokeObjectURL(url) } ``` ### 引导步骤 `tourSteps` 往新手引导里加几步,与通用步骤、流程自己的三步一起按 `order` 排:通用步骤是 0–60 与收尾的设置(900),流程的是 100 起,扩展的写 200 起。 ```ts export const simulation: FlowDesignerExtension = { id: '@acme/flow-simulation', tourSteps: [{ id: '@acme/flow-simulation.panel', target: { panel: 'simulation' }, title: '模拟运行', description: '在这里按步模拟流程的走向。', order: 200, }], about: [{ label: '模拟运行', value: '1.0.0' }], } ``` - `id` 用扩展 id 加点加名字;不能与别的步骤重名,也不能用留给 designer-shell 的 `shell.` 前缀,否则给 `extensions` 赋值就抛 `DesignerExtensionError`。 - `target` 指要高亮的目标:`{ panel }` 一块面板,`{ command }` 工具栏上的一条命令,`{ region }` 外框的区,`{ menubar }` 菜单栏上的一张菜单(菜单 id,如 `file`、`view`,窄屏时落在「菜单」钮上),`{ selector }` 设计器里的任意节点;不给就居中显示。目标看不见的那步自动跳过。 - `title`、`description` 可以是按上下文现算的函数,跟着语言变。 ### 关于 `about` 是设置「关于」页里的几行 `{ label, value }`,接在设计器自己的版本与许可证后面,适合写附加包的版本与授权;上面的示例里就写了一行。 ### setup `setup` 在设计器连上文档时调用,返回的函数在断开或扩展被换下时调用,适合加监听、开定时器这类要清理的事: ```ts const leaveTools: FlowDesignerExtension = { id: '@example/leave-tools', editors: { byControl: { 'leave.person': personEditor } }, commands, panels, setup: ({ designer }) => { const onDoubleClick = (event: Event): void => { const { id } = (event as FlowDesignerEventMap['node-dblclick']).detail designer.select({ nodes: [id], edges: designer.schema.edges.filter(edge => edge.source.node === id).map(edge => edge.id) }) } designer.addEventListener('node-dblclick', onDoubleClick) return () => designer.removeEventListener('node-dblclick', onDoubleClick) }, } ``` ## 冲突 节点类型、属性路径、编辑方式、命令 id、快捷键、面板的区名、物料来源 id、导入器或导出器的 id、设置分区 id、引导步骤 id、扩展 id 在两个扩展里重复时,给 `extensions` 赋值当场抛 `DesignerExtensionError`(从 `@xihan-ui/designer-kit` 导入),元素保持原来的扩展。错误上带冲突的名字空间、名字与两个扩展的 id: ```ts const other = { id: '@example/other', nodeTypes: [{ type: 'approve', size: { width: 160, height: 48 }, ports: 'both' as const }] } try { designer.extensions = [leave, other] } catch (error) { if (!(error instanceof DesignerExtensionError)) throw error // error.space 是 'node-type',error.key 是 'approve',error.extensions 是 ['@example/leave', '@example/other'] console.warn(error.message) } ``` --- 来源:https://flow.docs.xihanfun.com/guide/host # 与宿主集成 查看器与设计器只管显示与编辑,不发网络请求,也不碰存储与权限。把它们嵌进业务系统时,下面这些由宿主来做: | 事情 | 宿主做什么 | 元素给什么 | | --- | --- | --- | | 流程的保存 | 存流程定义,或换成引擎定义交给工作流引擎;管版本与发布 | `schema`、`schema-change`、`save`、`dirty`、`markSaved()`,预设的 `toWorkflowDefinition` | | 运行状态 | 从后端取实例的执行情况,换成节点的状态 | 查看器的 `statuses`、`setNodeStatuses()`、`highlightNodes()` | | 权限 | 决定谁能看、谁能改,服务端再复核 | `readonly`、`panels`、`extensions` | | 校验 | 保存前拦住有错误的定义,服务端复核 | `validate()`、`issues-change`、`validateDocument` | | 业务数据 | 受理人、表单这类要查宿主数据才能选的属性 | 设计器层扩展的属性编辑器 | ## 打开与保存 流程定义是一份完整的 JSON,原样存、原样取。对接 XiHan 工作流引擎时存的是引擎定义:打开时把取回的定义交给 `fromWorkflowDefinition`,结果赋给 `schema`;保存时把 `schema` 交给 `toWorkflowDefinition`,再交给后端。不用引擎时直接存取流程定义本身。 ```ts import type { XihanWorkflowDefinition } from '@xihan-flow/core/presets/xihan-workflow' import type { XhFlowDesigner } from '@xihan-flow/designer' import type { FlowNodeStatus, XhFlowViewer } from '@xihan-flow/runtime' import { readDocument, validateDocument } from '@xihan-flow/core' import { createXihanWorkflow, fromWorkflowDefinition, toWorkflowDefinition } from '@xihan-flow/core/presets/xihan-workflow' export async function openWorkflow(designer: XhFlowDesigner, code: string): Promise { const url = `/api/workflows/${encodeURIComponent(code)}` const response = await fetch(url) designer.schema = fromWorkflowDefinition(await response.json() as XihanWorkflowDefinition) designer.addEventListener('save', async () => { if (designer.validate().some(issue => issue.severity === 'error')) return const saving = designer.schema const saved = await fetch(url, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(toWorkflowDefinition(saving)), }) // 保存期间又改过的,不记为已保存 if (saved.ok && designer.schema === saving) designer.markSaved() }) } ``` - 保存前用 `validate()` 拦住错误级的问题,警告不拦。 - 保存是异步的,用户可能在等待期间接着改。示例在发出请求时记下当时的定义,成功后只有定义没再变才调用 `markSaved()`。 设计器在内存里改流程,页面关掉修改就没了。有未保存的修改时提醒用户: ```ts export function guardUnsaved(designer: XhFlowDesigner): () => void { const onBeforeUnload = (event: BeforeUnloadEvent): void => { if (designer.dirty) event.preventDefault() } window.addEventListener('beforeunload', onBeforeUnload) return () => window.removeEventListener('beforeunload', onBeforeUnload) } ``` ## 显示运行状态 业务页面用查看器显示某个实例走到了哪里:宿主从后端取实例里各节点的执行情况,换成「节点 id → 状态」交给 `statuses`,走过的节点可以再高亮出来。查看器为 `running`、`completed`、`waiting`、`faulted`、`canceled`、`compensated` 各画了一种样子,后端的状态名不同时在宿主这边换好,详见[查看器](./viewer#运行状态)。 ```ts /** 宿主接口返回的一条节点执行记录。 */ interface NodeRecord { readonly nodeId: string readonly status: FlowNodeStatus } export async function showInstance(viewer: XhFlowViewer, instanceId: string): Promise { const response = await fetch(`/api/workflow-instances/${encodeURIComponent(instanceId)}/nodes`) const records = await response.json() as NodeRecord[] viewer.statuses = Object.fromEntries(records.map(record => [record.nodeId, record.status])) viewer.highlightNodes(records.map(record => record.nodeId)) } ``` 实例还在运行时,宿主按自己的节奏重新取,用 `setNodeStatuses()` 只更新变了的节点。 ## 权限 - 只能看的用户:设计器设成 `readonly`,仍可选择、浏览与复制;只需要看的页面直接用查看器,它更轻。 - 只让调整局部的场景,用 `panels` 去掉物料等区,用 `commandLayout` 精简命令,用自定义属性编辑器限定可选的值。 - 这些都只在前端生效。流程的保存、发布与删除要在服务端判断权限,保存时服务端也应复核流程本身。 ## 服务端复核 `@xihan-flow/core` 与预设不依赖 DOM,存流程定义的宿主可以在 Node.js 后端直接读入并校验收到的定义,结论与设计器一致: ```ts /** 服务端收到流程定义时复核:读入不了或有错误级问题就拒收,返回问题码与路径。 */ export function reviewWorkflow(body: string): { valid: boolean, errors: string[] } { const read = readDocument(JSON.parse(body)) if (!read.ok) return { valid: false, errors: read.issues.map(issue => `${issue.code} @ ${issue.path.join('.')}`) } const errors = validateDocument(read.document, { extensions: [createXihanWorkflow()] }) .filter(issue => issue.severity === 'error') .map(issue => `${issue.code} @ ${issue.path.join('.')}`) return { valid: errors.length === 0, errors } } ``` 存引擎定义、后端是 XiHan.Framework 的,由引擎在发布时按它自己的规则把关,前端在提交前用 `validate()` 先拦一道。 ## 在前端框架里使用 查看器与设计器是原生 Web Components,任何框架都能用:在应用启动时调用一次 `defineFlowViewer()` 或 `defineFlowDesigner()`,之后像普通元素一样放进模板。流程定义、扩展这类对象要交给元素的属性,不要写成 attribute。 在 Vue 里,先告诉编译器 `xh-` 开头的标签是自定义元素,免得被当成组件去解析: ```ts // vite.config.ts 里 @vitejs/plugin-vue 的选项 vue({ template: { compilerOptions: { isCustomElement: tag => tag.startsWith('xh-'), }, }, }) ``` 对象用 `.prop` 修饰交给属性,事件用 `@` 监听: ```vue ``` React 19 起会把对象直接赋给自定义元素的同名属性;事件可以用 `ref` 拿到元素后 `addEventListener` 监听。 ## 文案与样式 - 内建文案缺省是英文。全站换语言时把同一语言的流程语言包(如 `flowZhCN`、`flowJaJP`)与 XiHan.UI 的语言包合并后交给 `setXhConfig`,写法见[快速开始](../quickstart#中文界面);单个实例的文案用它的 `translations` 覆写。预设的节点类型与属性另有文案,见 [XiHan 工作流引擎预设](./xihan-workflow#装上预设)。 - 样式只用 XiHan.UI 的设计令牌,元素跟着宿主的 UI 主题变化,宿主不用另写颜色。设计器与查看器铺满自身,宿主给元素定宽高。 ## 一个页面里放多个实例 元素不往 `window` 上挂对象,也不读写固定键名的本地存储,同一页面里的多个查看器、多个设计器互不影响,各自的扩展也只对自己生效。只有语言包是全局的:`setXhConfig` 对页面上所有元素生效。 --- 来源:https://flow.docs.xihanfun.com/guide/schema # 流程定义结构 流程定义(`FlowDocument`)是纯 JSON:一组节点、一组连线,加上文档级的业务数据。它不含也不执行任何代码,可以原样存进数据库、文件或交给接口。各字段的完整类型见[流程定义类型](../reference/schema)。 ## 文档 ```json { "version": 1, "nodes": [], "edges": [], "data": { "code": "expense", "name": "费用审批" } } ``` - `version` 是定义的版本号,读入时与 `FLOW_DOCUMENT_VERSION` 比对。 - `nodes` 与 `edges` 的数组顺序就是绘制层级,靠后的画在上层。 - `data` 是文档级的业务数据,比如流程编码与名称;有哪些字段由扩展登记的流程定义级属性决定,设计器里没选中任何对象时属性面板编辑的就是它们。 - `extensions` 放各扩展自己的数据,键是扩展 id,与业务数据分开(见下文)。 `createDocument(data?)` 返回一份空的流程定义。 ## 节点 ```json { "id": "dept", "type": "UserTask", "x": 176, "y": 100, "width": 160, "height": 48, "label": "部门经理审批", "data": { "properties": { "Assignees": ["部门经理"], "CompletionPolicy": "Any" } } } ``` - `id` 在文档内唯一,连线经它指向节点。 - `type` 是节点类型名,必须是扩展登记过的类型,否则校验报 `node.unknown-type`。节点类型决定节点的外观、端口、能连向哪里,以及 `data` 里有哪些属性。 - `x`、`y` 是节点左上角的坐标,`width`、`height` 是尺寸,单位都是像素;坐标原点在画布左上角,向右、向下为正。 - `label` 是节点上显示的文字。 - `data` 是业务数据,由节点类型的属性描述决定字段;设计器的属性面板按属性描述编辑它。 ## 连线 ```json { "id": "large", "source": { "node": "amount" }, "target": { "node": "gm" }, "label": "金额 > 1 万", "data": { "condition": "amount > 10000", "priority": 0 } } ``` - `source` 与 `target` 是起点与终点:`node` 是节点 id,`port` 是端口 id。不写 `port` 时取节点类型里第一个允许这一端的端口。 - `label` 是连线上显示的文字,`style.dashed` 画成虚线。 - `data` 是业务数据:有哪些字段由连线属性描述决定,可以随起点节点的类型不同(比如分支节点的出线才有条件与优先级)。 - `vertices` 是途经的折点,按从起点到终点的顺序排列。没写折点的连线由路由器自动求出直角折线,绕开途中的节点。 ## 业务数据与扩展数据 `data` 归业务:节点类型与属性描述决定它有哪些字段,宿主据此保存、提交与执行。`extensions` 归扩展:某个扩展要在节点、连线或文档上存自己的数据时,放在 `extensions` 里以扩展 id 为键的那一块,不混进业务数据。去掉一个扩展时,它在 `extensions` 里的数据原样保留,装回来时还能读到。 ## 读入与校验 从存储或接口取回的流程定义是未知输入,先交给 `readDocument`:它逐项检查字段类型、必填项与多余字段,通过时返回不与输入共享引用的副本,不通过时返回问题列表。`readDocument` 只检查结构;重复 id、悬空引用、端口约束与属性取值这类语义问题交给 `validateDocument`,它按传入的扩展检查,返回全部问题。 ```ts import { readDocument, validateDocument } from '@xihan-flow/core' import { extensions } from '../schemas/approval' export function load(json: string): { document: FlowDocument, errors: FlowIssue[] } { const read = readDocument(JSON.parse(json)) if (!read.ok) throw new Error(read.issues.map(issue => `${issue.code} @ ${issue.path.join('.')}`).join('\n')) const issues = validateDocument(read.document, { extensions, severity: { 'node.unreachable': 'off' }, }) return { document: read.document, errors: issues.filter(issue => issue.severity === 'error') } } ``` 每条问题带问题码、从文档根算起的路径、所在对象与严重程度,不带文案。`severity` 按问题码改写严重程度,写 `off` 即不报告。设计器自己会随编辑重新校验,把问题列在问题列表里;全部问题码与说明见[问题码](../reference/issues)。 ## 编辑操作 设计器里的每次修改都是一组纯 JSON 的编辑操作,`schema-change` 事件把它们连同新的流程定义一起交给宿主。`applyOperation` 与 `applyOperations` 在任何地方都能把操作应用到流程定义上,返回新定义与逆操作;按顺序应用逆操作就回到修改之前。删除节点时与它相连的连线一并删除,逆操作会把它们放回原位置。`replaceDocument` 整份换掉流程定义,逆操作换回原来那份;设计器的 JSON 编辑与导入整份定义都走它。 ```ts import { applyOperations } from '@xihan-flow/core' import { schema } from '../schemas/approval' const { document, inverse } = applyOperations(schema, [ { type: 'updateNode', id: 'gm', patch: { label: '总经理与财务审批' } }, { type: 'moveNodes', positions: [{ id: 'gm', x: 624, y: 0 }] }, ]) // 按顺序应用逆操作,回到修改之前 const restored = applyOperations(document, inverse).document ``` 操作只保证 id 唯一与被操作的对象存在;引用完整性、端口约束与属性取值仍由 `validateDocument` 负责。操作无法应用时抛 `FlowOperationError`,传入的流程定义保持不变。 --- 来源:https://flow.docs.xihanfun.com/guide/viewer # 查看器 `` 在业务页面里显示流程:按节点类型画出节点与连线,标出每个节点的运行状态与高亮,支持缩放、平移与定位节点。它只读,不改动流程定义。完整的属性、方法与事件见[查看器参考](../reference/viewer)。 ## 显示流程 调用 `defineFlowViewer()` 注册元素,再把扩展与流程定义交给 `extensions` 与 `schema` 属性,写法见[快速开始](../quickstart)。 - `extensions` 决定节点类型、端口与节点内容的渲染。没有登记的节点类型照样画成显示名称的方框,但它没有端口,连到它的连线画不出来。同一个扩展对象换了引用会当作卸下再装上,宿主要保持扩展数组与其中对象的引用稳定。 - 第一次拿到流程定义(`schema` 从无到有)时,查看器缩放并平移到整张图落在视口中央,小图不放大到 100% 以上;之后元素尺寸变化也跟着重新适配,直到调用 `setViewport` 或在画布上按下指针。 - 复杂数据走属性,不走 attribute;属性变了会自动重画。 ## 运行状态 `statuses` 是节点 id 到运行状态的映射,值为 `null` 或不写表示没有状态。也可以用方法逐步更新:`setNodeStatus(id, status)` 改一个,`setNodeStatuses(map)` 改一批,没列出的节点保持不变。 皮肤为这几种状态各画了一种样子: | 状态 | 用途 | | --- | --- | | `running` | 正在处理 | | `completed` | 已完成 | | `waiting` | 等待中 | | `faulted` | 出错 | | `canceled` | 已取消 | | `compensated` | 已补偿 | 状态是任意字符串,写在节点元素的 `data-status` 上。其它取值由宿主自己加样式,只用 XiHan.UI 的设计令牌: ```css xh-flow-viewer [data-part='node'][data-status='skipped'] { color: var(--xh-fg-muted); border-style: dashed; } ``` 节点元素上还有 `data-node-id` 与 `data-node-type`,高亮时带 `data-highlighted`。 ## 高亮与定位 `highlightNodes(ids)` 高亮一组节点、取消其余的高亮,`clearHighlights()` 全部取消;`highlighted` 属性是同一份数据。`scrollToNode(id)` 把节点居中,`revealNode(id)` 只在节点没整个落在视口里时才挪动。 ```ts const highlight = button('高亮大额路径', () => viewer.highlightNodes(['start', 'dept', 'amount', 'gm', 'approved'])) const clear = button('清除高亮', () => viewer.clearHighlights()) ``` ```ts const locate = button('定位到总经理审批', () => viewer.scrollToNode('gm')) const fit = button('适配', () => viewer.zoomToFit()) ``` `zoomIn`、`zoomOut`、`zoomToActual` 与 `zoomToFit` 对应工具栏上常见的四个按钮;`getViewport` 与 `setViewport` 读写视口,可以把视口存起来下次恢复。在画布上拖动或滚动滚轮平移,按住 Ctrl 或 ⌘ 滚动滚轮缩放;画布获得焦点后方向键平移,`+` 与 `-` 缩放,`0` 适配。 ## 事件 点击与右键都派发事件,`detail` 里是节点或连线的 id;右键事件还带浏览器视口坐标,事件可以取消,取消后浏览器的右键菜单不出现,宿主就能在那个位置弹自己的菜单。 ```ts viewer.addEventListener('node-click', (event) => { const { id } = (event as FlowViewerEventMap['node-click']).detail const node = schema.nodes.find(item => item.id === id) status.textContent = `点了「${node?.label}」,类型 ${node?.type}` }) viewer.addEventListener('node-contextmenu', (event) => { // 取消后浏览器的右键菜单不出现,宿主可以在 detail 的 x、y 处弹自己的菜单 event.preventDefault() status.textContent = `在「${(event as FlowViewerEventMap['node-contextmenu']).detail.id}」上按了右键` }) ``` 拖动平移之后松手不算点击。视口每次变化派发 `viewport-change`。 ## 自定义节点内容 运行时层的扩展可以给某个节点类型登记渲染函数 `nodeRenderers`:每次绘制都会调用,把内容写进节点元素。上下文里有节点、节点类型、要显示的文字、运行状态与是否高亮。没登记渲染的类型由 `defaultNodeRenderer` 画:节点类型给了 `icon` 时图标在前、名称在后,没给时只有名称;自己的渲染函数也可以先调它画出同样的内容再加别的。渲染函数只用 DOM API 建节点,不把数据拼进 `innerHTML`。 ```ts const assignees: FlowRuntimeExtension = { id: 'example/assignees', nodeRenderers: { UserTask: (container, { node, label }) => { const properties = node.data?.properties as { Assignees?: string[] } | undefined const title = document.createElement('strong') title.textContent = label const people = document.createElement('small') people.textContent = (properties?.Assignees ?? []).join('、') container.replaceChildren(title, document.createElement('br'), people) }, }, } ``` 装进 `extensions`,与节点类型所在的扩展放在一起: ```ts const viewer = document.createElement('xh-flow-viewer') viewer.extensions = [workflow, assignees] viewer.schema = schema ``` 同一个节点类型在两个扩展里都登记了渲染时,赋值当场抛 `DesignerExtensionError`,属性保持原值。设计器读的也是同一份渲染,画布上看到的与查看器一致。 ## 连线路由 没写折点的连线交给路由器求折线。缺省是直角路由:从端口引出、绕开途中的节点,找不到绕开全部节点的通路时退回不看障碍物的直角折线。`createOrthogonalRouter` 可以调整与节点保持的距离,也可以换成自己写的路由器,它拿到两端的锚点与要绕开的节点框,返回从起点到终点的完整折线: ```ts viewer.router = createOrthogonalRouter({ margin: 24 }) ``` ```ts const straight: FlowEdgeRouter = ({ source, target }) => [source.point, target.point] viewer.router = straight ``` ## 导出图片 `export(format, options)` 把画面上的流程图导出成图片,得到 `Blob`:`svg` 是能单独打开的 SVG 文件,`png` 在它上面栅格化。节点的框、图标与文字,连线、箭头与标签都画进去,颜色与字体取画面此刻的样式(运行状态的描边颜色也在),换成具体值写进文件,脱离页面也一样。颜色一律写成 sRGB 的 `#rrggbb`,带透明度的写进 `fill-opacity` / `stroke-opacity`,办公软件与矢量编辑器里也能打开;超出 sRGB 色域的颜色按通道裁剪。字体写在根上一次,各段文字只写与它不同的那几项。 | 选项 | 说明 | | --- | --- | | `scale` | 倍率,只对 PNG 有效,缺省 1;给高分屏看时写 2。图太大、超过浏览器画布的上限时自动降倍率,按比例缩小 | | `background` | 底色,任何 CSS 颜色;缺省取画布此刻的底色(画布透明时往外找第一层不透明的),写 `transparent` 不铺 | | `padding` | 四周的留白,缺省 24 | ```ts const png = await viewer.export('png', { scale: 2 }) ``` 自定义节点内容只按节点的文字导出一行。PNG 里的文字用本机的系统字体画,不加载网页字体;SVG 文件里写的是字体名,打开它的机器上有这个字体就用它。流程图是空的时以 `RangeError` 拒绝。 ## 文案 查看器只有一句给读屏的名称,缺省是英文 `Flow diagram`,中文语言包里是「流程图」。单个实例要改时写 `translations`,它压在全局配置之上。 --- 来源:https://flow.docs.xihanfun.com/guide/xihan-workflow # XiHan 工作流引擎预设 XiHan.Framework 的[工作流引擎](https://framework.docs.xihanfun.com/guide/workflow)按流程定义 JSON 运行流程。`@xihan-flow/core/presets/xihan-workflow` 把引擎的每种活动做成一个节点类型,属性键与引擎读的一致,规则与引擎发布时的检查对齐,并能与引擎定义互转:设计器里画好的流程换成引擎定义交给后端保存、发布,引擎里的定义也能读回设计器。全部节点类型、属性与问题码见[预设参考](../reference/xihan-workflow)。 ## 装上预设 `createXihanWorkflow` 按文案生成一个 core 层扩展,交给查看器、设计器与 `validateDocument` 都可以。不传文案是英文,也可以导入现成的英文扩展 `xihanWorkflow`。文案与流程的语言包一样按 XiHan.UI 的语言一一对应:`xihanWorkflowZhCN`、`xihanWorkflowZhTW`、`xihanWorkflowJaJP`、`xihanWorkflowKoKR`、`xihanWorkflowDeDE`、`xihanWorkflowFrFR`、`xihanWorkflowEsES`、`xihanWorkflowPtBR`、`xihanWorkflowRuRU` 与 `xihanWorkflowEnUS`,换哪种语言就把哪一份传给 `texts`;只改几处时传其中几个键,其余照英文。17 种节点类型各配了一枚 `@xihan-ui/icons` 的图标,画布上的节点与物料条目都画在文字前面,对应关系见[参考页](../reference/xihan-workflow)。 ```ts /** 节点类型、属性与规则:XiHan 工作流引擎的节点预设,文案用中文。 */ export const workflow = createXihanWorkflow({ texts: xihanWorkflowZhCN }) export const extensions = [workflow] ``` - 节点类型与属性的文字写在扩展里。切换语言就换一个扩展对象,id 相同,设计器按卸下再装上处理,流程定义不变。 - 物料的类别与属性的分组只给出键,文字在文案的 `categories` 与 `groups` 里,交给设计器的 `paletteGroupLabels` 与 `propertyGroupLabels`,写法见[设计器](./designer#放上页面)。 - 节点的类型名就是引擎的活动类型,如 `Start`、`UserTask`、`Http`。 ## 节点与属性 节点的属性按引擎的字段存放:活动属性写在 `data.properties` 下,键名与引擎活动读的一样,如人工任务的 `Assignees`、HTTP 请求的 `Url`;超时、失败续行与重试策略是节点级字段,写在 `data.timeoutSeconds`、`data.continueOnError` 与 `data.retryPolicy`。节点名称就是节点的标签。 属性面板按基本、参与人、表单与执行分组。必填的属性没填、取值超出范围或不在选项里时,校验照常报问题。 连线的属性随源节点类型:从条件分支连出的连线有条件、优先级与默认分支,并行分支沿全部出线流转,连出的连线没有条件。默认分支画成虚线:外观由 `isDefault` 推出,勾选、取消时跟着变,不另存一份样式。流程编码、名称、分类、描述、是否启用补偿与启动变量是流程定义级的属性,写在流程定义的 `data` 里,什么都没选中时属性面板编辑它们。 ## 受理人规则 人工任务的受理人规则、兜底受理人规则与抄送规则用预设登记的编辑方式 `XIHAN_WORKFLOW_ASSIGNEE_RULES`,取值是 `{ type, values }` 的数组,与引擎的[受理人规则](https://framework.docs.xihanfun.com/guide/workflow#受理人规则)一致:引擎内置 `User`、`Starter`、`Expression` 三种,角色、部门等由应用注册。预设检查取值的形状,并要求 `User` 至少有一个受理人、`Expression` 恰好一条非空的表达式;应用注册的类型只检查形状。 选人、选角色要查宿主自己的数据,所以这种编辑方式的编辑器由宿主在设计器层的扩展里按种类提供;没有提供时,属性面板里这一行只读。下面是一个最简单的写法,把规则写成 JSON: ```ts const assigneeRulesEditor: FlowPropertyEditor = (initial) => { const input = document.createElement('textarea') input.rows = 3 input.setAttribute('aria-label', initial.descriptor.label ?? '') let context = initial input.addEventListener('change', () => { try { context.setValue(input.value.trim() === '' ? undefined : JSON.parse(input.value)) input.removeAttribute('aria-invalid') context.commit() } catch { input.setAttribute('aria-invalid', 'true') } }) const update = (next: typeof initial): void => { context = next if (document.activeElement !== input) input.value = next.value === undefined ? '' : JSON.stringify(next.value) input.readOnly = next.readonly } update(initial) return { element: input, update } } export const assigneeRules: FlowDesignerExtension = { id: '@example/assignee-rules', editors: { byControl: { [XIHAN_WORKFLOW_ASSIGNEE_RULES]: assigneeRulesEditor } }, } ``` 人工任务还保留着旧的受理人写法:直接列受理人(`Assignees`)、受理人表达式(`AssigneesExpression`)与抄送人(`CcUserIds`)。只用受理人规则的应用写 `createXihanWorkflow({ legacyAssignees: false })`,属性面板里就不再显示这三项;定义里已有的值照旧读入、写回,往返不丢,校验也照旧认它们。缺省是 `true`,三项都显示。 把 `assigneeRules` 与预设一起放进设计器的 `extensions` 即可。编辑器的写法见[扩展](./extensions#属性编辑器)。 ## 启动变量 流程定义的启动变量用预设登记的编辑方式 `XIHAN_WORKFLOW_START_VARIABLES`,取值是变量声明的数组,与引擎读的一致:`name` 是变量名;`required` 为 `true` 时,启动流程缺了这个变量引擎就拒绝启动;`defaultValue` 在启动时没有提供、又不是必填时生效;`description` 是说明;`type` 只是给人看的类型提示,引擎不按它检查。预设检查取值的形状:变量名不能为空、不能重名,`required` 是布尔,`type` 与 `description` 是字符串。 这种编辑方式的编辑器是设计器内建的:属性面板里排成一张表,一个变量一行,列是变量名、必填、缺省值与说明,可以增删。三个文字列有最小宽度,面板窄时表格区横向滚动,行号与把手留在两侧。缺省值按 JSON 读写,字符串要带引号,如 `"lisi"`、`1000`、`true`;写不成 JSON 时这一格标为无效、不写入,清空即不设缺省值。变量上表里没有的字段(如 `type`)原样保留。要换一种编辑器时,与受理人规则一样在设计器层的扩展里按种类登记,它压过内建的那个。 ## 规则 预设带着几条规则,检查引擎发布定义时会拒绝、而通用校验覆盖不到的情况: - 流程里没有结束或终止节点,报警告。 - 条件分支有不止一条默认分支,报错误;有带条件的出线、却既没有默认分支也没有无条件的出线,报警告。 - 人工任务没有配置受理人规则、受理人或受理人表达式之一,报错误。 问题码以 `xihan-workflow.` 开头,说明在预设的文案里,严重程度可以经设计器的 `validateOptions` 改写。 ## 与引擎定义互转 `toWorkflowDefinition` 把流程定义换成引擎的定义 JSON,`fromWorkflowDefinition` 反过来。下面把费用审批换成引擎定义,再读回查看器;去掉坐标读回时,节点按连线自左向右重新排开: ```ts const definition: XihanWorkflowDefinition = toWorkflowDefinition(schema) const json = JSON.stringify(definition, null, 2) ``` ```ts const readBack = button('去掉坐标后读回', () => { const saved = JSON.parse(json) as XihanWorkflowDefinition const { designerLayout: _, ...extraProperties } = saved.extraProperties ?? {} viewer.schema = fromWorkflowDefinition({ ...saved, extraProperties }) }) const original = button('按保存的坐标读回', () => { viewer.schema = fromWorkflowDefinition(JSON.parse(json) as XihanWorkflowDefinition) }) ``` 字段这样对应: | 流程定义 | 引擎定义 | | --- | --- | | 节点的 `type` | 节点的 `activityType` | | 节点的 `label` | 节点的 `name` | | 节点 `data` 里的字段(`properties`、`retryPolicy`、`timeoutSeconds`、`continueOnError` 等) | 节点上的同名字段 | | 连线的 `source.node`、`target.node` | 连线的 `sourceNodeId`、`targetNodeId` | | 连线的 `label` | 连线的 `name` | | 连线 `data` 里的字段(`condition`、`priority`、`isDefault` 等) | 连线上的同名字段 | | 流程定义 `data` 里的字段(`code`、`name`、`category` 等) | 定义顶层的同名字段 | | 节点坐标 | `extraProperties.designerLayout` | | 连线的拐点与连线自己写的外观(由 `isDefault` 推出的虚线不写) | `extraProperties.designerEdges` | | 节点、连线与流程定义上的扩展数据 | `extraProperties.designerExtensions` | - `extraProperties` 里这三项是 JSON 字符串,引擎不读,原样存着。节点的宽高不存,读回时按活动类型取。 - 读回时节点没有坐标,就按连线自左向右分层排出坐标,所以引擎里用代码写的定义也能直接打开。连线没有 id 时补成「源->目标#序号」,值为 `null` 的字段去掉;结构不对时抛 `TypeError`。 - 引擎定义里没列出的字段原样保留,往返之后不丢。示例里的费用审批换成引擎定义再读回与原来一致,文档站的检查会核对这一点。 保存时把 `toWorkflowDefinition(designer.schema)` 交给后端,引擎在发布时再按它自己的规则把关;打开时把后端给的定义交给 `fromWorkflowDefinition`,结果赋给 `schema`。