diff --git a/examples/feature-examples/src/pages/extensions/proximity-connect/index.tsx b/examples/feature-examples/src/pages/extensions/proximity-connect/index.tsx index c4a123fdc..71d78e68f 100644 --- a/examples/feature-examples/src/pages/extensions/proximity-connect/index.tsx +++ b/examples/feature-examples/src/pages/extensions/proximity-connect/index.tsx @@ -4,6 +4,7 @@ import { ProximityConnect } from '@logicflow/extension' import { Space, Input, + InputNumber, Button, Card, Divider, @@ -11,6 +12,7 @@ import { Col, Form, Switch, + Select, } from 'antd' import { useEffect, useRef, useState } from 'react' import styles from './index.less' @@ -111,6 +113,9 @@ export default function ProximityConnectExtension() { const [distance, setDistance] = useState(100) const [reverse, setReverse] = useState(false) const [enable, setEnable] = useState(true) + const [mode, setMode] = useState<'node' | 'anchor' | 'default'>('default') + const [virtualStroke, setVirtualStroke] = useState('#acacac') + const [virtualDash, setVirtualDash] = useState('10,10') useEffect(() => { if (!lfRef.current) { const lf = new LogicFlow({ @@ -129,12 +134,24 @@ export default function ProximityConnectExtension() { enable, distance, reverseDirection: reverse, + type: mode, }, }, }) lf.render(data) lfRef.current = lf + + // 初始化插件的阈值与样式,避免 options.distance 未绑定到 thresholdDistance 的影响 + const pc = lf.extension.proximityConnect as ProximityConnect + pc.setThresholdDistance(distance) + pc.setReverseDirection(reverse) + pc.setEnable(enable) + pc.type = mode + pc.setVirtualEdgeStyle({ + stroke: virtualStroke, + strokeDasharray: virtualDash, + }) } }, []) @@ -143,11 +160,13 @@ export default function ProximityConnectExtension() { - { - setDistance(+e.target.value) + min={1} + onChange={(val) => { + const next = Number(val || 0) + setDistance(next) }} /> + +
diff --git a/examples/feature-examples/src/pages/extensions/snapshot/index.tsx b/examples/feature-examples/src/pages/extensions/snapshot/index.tsx index 673477235..2e05f076a 100644 --- a/examples/feature-examples/src/pages/extensions/snapshot/index.tsx +++ b/examples/feature-examples/src/pages/extensions/snapshot/index.tsx @@ -76,6 +76,20 @@ export default function SnapshotExample() { const [quality, setQuality] = useState() // 图片质量 const [partial, setPartial] = useState(false) // 导出局部渲染 + // 快照插件样式控制 + const [useGlobalRules, setUseGlobalRules] = useState(true) // 是否注入全局样式 + const [customCssRules, setCustomCssRules] = useState(` + .uml-wrapper { + line-height: 1.2; + text-align: center; + color: blue; + } + `) // 自定义样式规则,将叠加到导出图片中 + + // 画布尺寸安全参数 + const [safetyFactor, setSafetyFactor] = useState(1.1) // 画布导出安全系数 + const [safetyMargin, setSafetyMargin] = useState(40) // 画布导出安全边距 + const [blobData, setBlobData] = useState('') const [base64Data, setBase64Data] = useState('') @@ -118,16 +132,12 @@ export default function SnapshotExample() { }) }) - // 默认开启css样式 - lf.extension.snapshot.useGlobalRules = true - // 不会覆盖css样式,会叠加,customCssRules优先级高 - lf.extension.snapshot.customCssRules = ` - .uml-wrapper { - line-height: 1.2; - text-align: center; - color: blue; - } - ` + // 设置快照插件样式参数(通过类型断言访问扩展实例属性) + const snapshotExt = lf.extension?.snapshot as unknown as Snapshot + if (snapshotExt) { + snapshotExt.useGlobalRules = useGlobalRules + snapshotExt.customCssRules = customCssRules + } lf.render(data) lf.translateCenter() @@ -146,8 +156,17 @@ export default function SnapshotExample() { height, padding, quality, + safetyFactor, + safetyMargin, } console.log(params, 'params') + // 在导出前更新快照扩展的样式控制参数 + const snapshotExt = lfRef.current?.extension + ?.snapshot as unknown as Snapshot + if (snapshotExt) { + snapshotExt.useGlobalRules = useGlobalRules + snapshotExt.customCssRules = customCssRules + } await lfRef.current?.getSnapshot(fileName, params) // await lfRef.current?.extension.snapshot?.getSnapshot(fileName, params) // 测试 @@ -172,6 +191,15 @@ export default function SnapshotExample() { height, padding, quality, + safetyFactor, + safetyMargin, + } + // 在预览前更新快照扩展的样式控制参数 + const snapshotExt = lfRef.current.extension + ?.snapshot as unknown as Snapshot + if (snapshotExt) { + snapshotExt.useGlobalRules = useGlobalRules + snapshotExt.customCssRules = customCssRules } lfRef.current .getSnapshotBlob(backgroundColor, fileType, params) @@ -204,6 +232,15 @@ export default function SnapshotExample() { height, padding, quality, + safetyFactor, + safetyMargin, + } + // 在预览前更新快照扩展的样式控制参数 + const snapshotExt = lfRef.current.extension + ?.snapshot as unknown as Snapshot + if (snapshotExt) { + snapshotExt.useGlobalRules = useGlobalRules + snapshotExt.customCssRules = customCssRules } const result = await lfRef.current.getSnapshotBase64( 'white', @@ -287,6 +324,16 @@ export default function SnapshotExample() { value={height} onChange={(value) => handleInputChange(value, 'height')} /> + setSafetyFactor(value ?? 1.1)} + /> + setSafetyMargin(value ?? 40)} + />

@@ -307,6 +354,20 @@ export default function SnapshotExample() { /> 导出局部渲染: setPartial(partial)} /> + 注入全局样式: + setUseGlobalRules(checked)} + /> + +

+ + setCustomCssRules(e.target.value)} + /> diff --git a/packages/extension/src/bpmn-adapter/index.ts b/packages/extension/src/bpmn-adapter/index.ts index 33c1b8ef2..60b872114 100644 --- a/packages/extension/src/bpmn-adapter/index.ts +++ b/packages/extension/src/bpmn-adapter/index.ts @@ -2,6 +2,20 @@ import { getBpmnId } from './bpmnIds' import { handleAttributes, lfJson2Xml } from './json2xml' import { lfXml2Json } from './xml2json' +/** + * 模块说明(BPMN Adapter) + * + * 该模块负责在 LogicFlow 内部图数据(GraphData)与 BPMN XML/JSON 之间进行双向转换: + * - adapterOut:将 LogicFlow 图数据转换为 BPMN JSON(随后由 json2xml 转为 XML) + * - adapterIn:将 BPMN JSON 转换为 LogicFlow 图数据(如果是 XML,则先经 xml2json 转为 JSON) + * + * 设计要点与特殊处理: + * - BPMN XML 的属性在 JSON 中以前缀 '-' 表示(如 '-id'、'-name'),本模块严格遵循该约定。 + * - XML 中同名子节点可能出现多次,xml2json 解析后会以数组表示;本模块对数组与单对象场景均做兼容处理。 + * - BPMN 画布坐标以元素左上角为基准,而 LogicFlow 以元素中心为基准;转换时需进行坐标基准转换。 + * - 文本内容在导出时进行 XML 转义,在导入时进行反转义,确保特殊字符(如 <, >, & 等)能被正确保留。 + */ + import { ExclusiveGatewayConfig, StartEventConfig, @@ -10,6 +24,12 @@ import { UserTaskConfig, } from '../bpmn/constant' +/** + * LogicFlow 节点配置(导入/导出过程中使用的中间结构) + * - id/type/x/y:节点基本信息 + * - text:节点文本的中心坐标与内容(值为未转义的原始字符串) + * - properties:节点的额外属性(会保留到 BPMN 的扩展字段) + */ type NodeConfig = { id: string properties?: Record @@ -23,11 +43,21 @@ type NodeConfig = { y: number } +/** + * 点坐标结构(用于边的路径点) + */ type Point = { x: number y: number } +/** + * LogicFlow 边配置(导入/导出过程中使用的中间结构) + * - id/type/sourceNodeId/targetNodeId:边的基本信息 + * - pointsList:边的路径点(用于 BPMN 的 di:waypoint) + * - text:边文本的位置与内容(值为未转义的原始字符串) + * - properties:边的扩展属性 + */ type EdgeConfig = { id: string sourceNodeId: string @@ -50,6 +80,9 @@ type EdgeConfig = { properties: Record } +/** + * BPMN 元素类型映射(用于在 JSON 中定位具体的 BPMN 节点类型) + */ enum BpmnElements { START = 'bpmn:startEvent', END = 'bpmn:endEvent', @@ -59,6 +92,11 @@ enum BpmnElements { FLOW = 'bpmn:sequenceFlow', } +/** + * BPMN 过程元素的标准属性键列表 + * - 在解析 `processValue` 时,这些键会被视为标准属性而非扩展属性; + * - 其余未在列表中的键会进入 LogicFlow 的 `properties` 中,以保留扩展数据。 + */ const defaultAttrs = [ '-name', '-id', @@ -78,6 +116,10 @@ const defaultAttrs = [ * 这意味着出现在这个数组里的字段当它的值是数组或是对象时不会被视为一个节点而是一个属性 * @reference node type reference https://www.w3schools.com/xml/dom_nodetype.asp */ +/** + * 导出至 BPMN JSON 时,作为属性保留的字段列表 + * - 当这些字段的值为对象或数组时,仍视为属性(在 JSON 中以 '-' 前缀表示),而非子节点。 + */ const defaultRetainedFields = [ 'properties', 'startPoint', @@ -85,11 +127,32 @@ const defaultRetainedFields = [ 'pointsList', ] +const unescapeXml = (text: string) => + String(text || '') + .replace(/</g, '<') + .replace(/>/g, '>') + .replace(/&/g, '&') + .replace(/"/g, '"') + .replace(/'/g, "'") + +/** + * 将普通 JSON 转换为 XML 风格 JSON(xmlJson) + * 输入:任意 JSON 对象;可选的保留属性字段 retainedFields + * 输出:遵循 XML 属性前缀约定的 xmlJson(属性键以 '-' 开头) + * 规则: + * - 原始字符串直接返回;数组逐项转换;对象根据键类型决定是否加 '-' 前缀。 + * - 保留字段(fields)中出现的键以属性形式(带 '-')保留,否则视为子节点。 + */ function toXmlJson(retainedFields?: string[]) { const fields = retainedFields ? defaultRetainedFields.concat(retainedFields) : defaultRetainedFields return (json: string | any[] | Record) => { + /** + * 递归转换核心方法 + * @param obj 输入对象/数组/字符串 + * @returns 转换后的 xmlJson + */ function ToXmlJson(obj: string | any[] | Record) { const xmlJson = {} if (typeof obj === 'string') { @@ -123,7 +186,9 @@ function toXmlJson(retainedFields?: string[]) { } /** - * 将xmlJson转换为普通的json,在内部使用。 + * 将 XML 风格 JSON(xmlJson)转换回普通 JSON(内部使用) + * 输入:遵循 '-' 属性前缀约定的 xmlJson + * 输出:去除前缀并恢复原有结构的普通 JSON */ function toNormalJson(xmlJson) { const json = {} @@ -152,6 +217,18 @@ function toNormalJson(xmlJson) { * 2)如果只有一个子元素,json中表示为正常属性 * 3)如果是多个子元素,json中使用数组存储 */ +/** + * 将 LogicFlow 图数据中的节点与边转换为 BPMN 的 process 数据结构 + * 输入: + * - bpmnProcessData:输出目标对象(会被填充 '-id'、各 bpmn:* 节点以及 sequenceFlow) + * - data:LogicFlow 图数据(nodes/edges) + * - retainedFields:可选保留属性字段,用于控制属性与子节点的映射 + * 输出:直接修改 bpmnProcessData + * 特殊处理: + * - 节点文本(node.text.value)作为 BPMN 的 '-name' 属性; + * - 维护 incoming/outgoing 的顺序,保证解析兼容性; + * - 多子元素时转为数组结构(XML 约定)。 + */ function convertLf2ProcessData( bpmnProcessData, data, @@ -223,6 +300,16 @@ function convertLf2ProcessData( /** * adapterOut 设置bpmn diagram信息 */ +/** + * 将 LogicFlow 图数据转换为 BPMN 的图形数据(BPMNDiagram/BPMNPlane 下的 Shape 与 Edge) + * 输入: + * - bpmnDiagramData:输出目标对象(填充 BPMNShape/BPMNEdge) + * - data:LogicFlow 图数据(nodes/edges) + * 输出:直接修改 bpmnDiagramData + * 特殊处理: + * - 节点坐标从中心点转换为左上角基准; + * - 文本的显示边界(Bounds)根据文本长度近似计算,用于在 BPMN 渲染器正确定位标签。 + */ function convertLf2DiagramData(bpmnDiagramData, data) { bpmnDiagramData['bpmndi:BPMNEdge'] = data.edges.map((edge) => { const edgeId = edge.id @@ -287,26 +374,36 @@ function convertLf2DiagramData(bpmnDiagramData, data) { /** * 将bpmn数据转换为LogicFlow内部能识别数据 */ +/** + * 将 BPMN JSON 转换为 LogicFlow 可识别的图数据 + * 输入: + * - bpmnData:包含 'bpmn:definitions' 的 BPMN JSON + * 输出:{ nodes, edges }:LogicFlow 的 GraphConfigData + * 特殊处理: + * - 若缺失 process 或 plane,返回空数据以避免渲染错误。 + */ function convertBpmn2LfData(bpmnData) { let nodes: NodeConfig[] = [] let edges: EdgeConfig[] = [] const definitions = bpmnData['bpmn:definitions'] if (definitions) { + // 如后续需多图/多流程支持,再扩展为遍历与合并,现在看起来是没有这个场景 const process = definitions['bpmn:process'] + const diagram = definitions['bpmndi:BPMNDiagram'] + const plane = diagram?.['bpmndi:BPMNPlane'] + if (!process || !plane) { + return { nodes, edges } + } Object.keys(process).forEach((key) => { if (key.indexOf('bpmn:') === 0) { const value = process[key] if (key === BpmnElements.FLOW) { - const bpmnEdges = - definitions['bpmndi:BPMNDiagram']['bpmndi:BPMNPlane'][ - 'bpmndi:BPMNEdge' - ] + const edgesRaw = plane['bpmndi:BPMNEdge'] + const bpmnEdges = Array.isArray(edgesRaw) ? edgesRaw : edgesRaw edges = getLfEdges(value, bpmnEdges) } else { - const shapes = - definitions['bpmndi:BPMNDiagram']['bpmndi:BPMNPlane'][ - 'bpmndi:BPMNShape' - ] + const shapesRaw = plane['bpmndi:BPMNShape'] + const shapes = Array.isArray(shapesRaw) ? shapesRaw : shapesRaw nodes = nodes.concat(getLfNodes(value, shapes, key)) } } @@ -318,6 +415,14 @@ function convertBpmn2LfData(bpmnData) { } } +/** + * 根据 BPMN 的 process 子节点与 plane 中的 BPMNShape 生成 LogicFlow 节点数组 + * 输入: + * - value:当前类型(如 bpmn:userTask)的值,可能为对象或数组 + * - shapes:plane['bpmndi:BPMNShape'],可能为对象或数组 + * - key:当前处理的 BPMN 类型键名(如 'bpmn:userTask') + * 输出:LogicFlow 节点数组 + */ function getLfNodes(value, shapes, key) { const nodes: NodeConfig[] = [] if (Array.isArray(value)) { @@ -349,10 +454,23 @@ function getLfNodes(value, shapes, key) { return nodes } +/** + * 将单个 BPMNShape 与其对应的 process 节点合成为 LogicFlow 节点配置 + * 输入: + * - shapeValue:plane 中的 BPMNShape(包含 Bounds 与可选 BPMNLabel) + * - type:BPMN 节点类型键(如 'bpmn:userTask') + * - processValue:process 中对应的节点对象(包含 '-id'、'-name' 等) + * 输出:LogicFlow NodeConfig + * 特殊处理: + * - 坐标从左上角转为中心点; + * - 文本从 '-name' 读取并进行 XML 实体反转义; + * - 文本位置优先使用 BPMNLabel 的 Bounds。 + */ function getNodeConfig(shapeValue, type, processValue) { let x = Number(shapeValue['dc:Bounds']['-x']) let y = Number(shapeValue['dc:Bounds']['-y']) - const name = processValue['-name'] + // 反转义 XML 实体,确保导入后文本包含特殊字符时能被完整还原 + const name = unescapeXml(processValue['-name']) const shapeConfig = BpmnAdapter.shapeConfigMap.get(type) if (shapeConfig) { x += shapeConfig.width / 2 @@ -399,6 +517,13 @@ function getNodeConfig(shapeValue, type, processValue) { return nodeConfig } +/** + * 根据 BPMN 的 sequenceFlow 与 BPMNEdge 生成 LogicFlow 边数组 + * 输入: + * - value:process['bpmn:sequenceFlow'],对象或数组 + * - bpmnEdges:plane['bpmndi:BPMNEdge'],对象或数组 + * 输出:LogicFlow 边数组 + */ function getLfEdges(value, bpmnEdges) { const edges: EdgeConfig[] = [] if (Array.isArray(value)) { @@ -427,11 +552,31 @@ function getLfEdges(value, bpmnEdges) { return edges } +/** + * 将单个 BPMNEdge 与其对应的 sequenceFlow 合成为 LogicFlow 边配置 + * 输入: + * - edgeValue:BPMNEdge(包含 di:waypoint 以及可选 BPMNLabel/Bounds) + * - processValue:sequenceFlow(包含 '-id'、'-sourceRef'、'-targetRef'、'-name' 等) + * 输出:LogicFlow EdgeConfig + * 特殊处理: + * - 文本从 '-name' 读取并进行 XML 实体反转义; + * - 若缺失 BPMNLabel,则以边的几何中心近似作为文本位置; + * - pointsList 由 waypoints 转换得到,数值类型统一为 Number。 + */ function getEdgeConfig(edgeValue, processValue): EdgeConfig { let text - const textVal = processValue['-name'] ? `${processValue['-name']}` : '' + // 反转义 XML 实体,确保导入后文本包含特殊字符时能被完整还原 + const textVal = processValue['-name'] + ? unescapeXml(`${processValue['-name']}`) + : '' if (textVal) { - const textBounds = edgeValue['bpmndi:BPMNLabel']['dc:Bounds'] + let textBounds + if ( + edgeValue['bpmndi:BPMNLabel'] && + edgeValue['bpmndi:BPMNLabel']['dc:Bounds'] + ) { + textBounds = edgeValue['bpmndi:BPMNLabel']['dc:Bounds'] + } // 如果边文本换行,则其偏移量应该是最长一行的位置 let textLength = 0 textVal.split('\n').forEach((textSpan) => { @@ -440,10 +585,26 @@ function getEdgeConfig(edgeValue, processValue): EdgeConfig { } }) - text = { - value: textVal, - x: Number(textBounds['-x']) + (textLength * 10) / 2, - y: Number(textBounds['-y']) + 7, + if (textBounds) { + text = { + value: textVal, + x: Number(textBounds['-x']) + (textLength * 10) / 2, + y: Number(textBounds['-y']) + 7, + } + } else { + // 兼容缺少 BPMNLabel 的图:使用边的几何中心作为文本位置 + const waypoints = edgeValue['di:waypoint'] || [] + const first = waypoints[0] + const last = waypoints[waypoints.length - 1] || first + const centerX = + (Number(first?.['-x'] || 0) + Number(last?.['-x'] || 0)) / 2 + const centerY = + (Number(first?.['-y'] || 0) + Number(last?.['-y'] || 0)) / 2 + text = { + value: textVal, + x: centerX, + y: centerY, + } } } let properties @@ -474,6 +635,14 @@ function getEdgeConfig(edgeValue, processValue): EdgeConfig { return edge } +/** + * BpmnAdapter:基础适配器 + * + * 作用:在 LogicFlow 数据与 BPMN JSON 之间进行转换,并注入 adapterIn/adapterOut 钩子。 + * - processAttributes:导出时 BPMN process 的基础属性(可配置 isExecutable、id 等)。 + * - definitionAttributes:导出时 BPMN definitions 的基础属性与命名空间声明。 + * - shapeConfigMap:不同 BPMN 元素类型的默认宽高,用于坐标/Bounds 计算。 + */ class BpmnAdapter { static pluginName = 'bpmn-adapter' static shapeConfigMap = new Map() @@ -494,6 +663,11 @@ class BpmnAdapter { [key: string]: any } + /** + * 构造函数 + * - 注入 LogicFlow 的 adapterIn/adapterOut(处理 JSON 方向的适配) + * - 初始化 process 与 definitions 的基础属性 + */ constructor({ lf }) { lf.adapterIn = (data) => this.adapterIn(data) lf.adapterOut = (data, retainedFields?: string[]) => @@ -524,6 +698,13 @@ class BpmnAdapter { * ["properties", "startPoint", "endPoint", "pointsList"]合并, * 这意味着出现在这个数组里的字段当它的值是数组或是对象时不会被视为一个节点而是一个属性。 */ + /** + * adapterOut:将 LogicFlow 图数据转换为 BPMN JSON + * 输入: + * - data:LogicFlow GraphData + * - retainedFields:扩展属性保留字段 + * 输出:BPMN JSON(包含 definitions/process/diagram/plane) + */ adapterOut = (data, retainedFields?: string[]) => { const bpmnProcessData = { ...this.processAttributes } convertLf2ProcessData(bpmnProcessData, data, retainedFields) @@ -543,6 +724,11 @@ class BpmnAdapter { } return bpmnData } + /** + * adapterIn:将 BPMN JSON 转换为 LogicFlow 图数据 + * 输入:bpmnData:BPMN JSON + * 输出:GraphConfigData(nodes/edges) + */ adapterIn = (bpmnData) => { if (bpmnData) { return convertBpmn2LfData(bpmnData) @@ -571,9 +757,19 @@ BpmnAdapter.shapeConfigMap.set(BpmnElements.USER, { height: UserTaskConfig.height, }) +/** + * BpmnXmlAdapter:XML 适配器(继承 BpmnAdapter) + * + * 作用:处理 XML 输入/输出的适配,使用 xml2json/json2xml 完成格式转换。 + * 特殊处理:在 XML 导入前对 name 属性的非法字符进行预处理转义,提升容错。 + */ class BpmnXmlAdapter extends BpmnAdapter { static pluginName = 'bpmnXmlAdapter' + /** + * 构造函数 + * - 覆盖 LogicFlow 的 adapterIn/adapterOut,使其面向 XML 输入与输出。 + */ constructor(data) { super(data) const { lf } = data @@ -581,10 +777,46 @@ class BpmnXmlAdapter extends BpmnAdapter { lf.adapterOut = this.adapterXmlOut } + // 预处理:修复属性值中非法的XML字符(仅针对 name 属性) + /** + * 预处理 XML:仅对 name 属性值进行非法字符转义(<, >, &),避免 DOM 解析失败。 + * 注意:不影响已合法的实体(如 &),仅在属性值中生效,不修改其它内容。 + */ + private sanitizeNameAttributes(xml: string): string { + return xml.replace(/name="([^"]*)"/g, (_, val) => { + const safe = val + .replace(/&(?!#?\w+;)/g, '&') + .replace(//g, '>') + return `name="${safe}"` + }) + } + + /** + * adapterXmlIn:将 BPMN XML 转换为 LogicFlow 图数据 + * 输入:bpmnData:XML 字符串或对象 + * 步骤: + * 1) 若为字符串,先对 name 属性进行预处理转义; + * 2) 使用 lfXml2Json 转换为 BPMN JSON; + * 3) 调用基础 adapterIn 转换为 GraphData。 + */ adapterXmlIn = (bpmnData) => { - const json = lfXml2Json(bpmnData) + const xmlStr = + typeof bpmnData === 'string' + ? this.sanitizeNameAttributes(bpmnData) + : bpmnData + const json = lfXml2Json(xmlStr) return this.adapterIn(json) } + /** + * adapterXmlOut:将 LogicFlow 图数据转换为 BPMN XML + * 输入: + * - data:GraphData + * - retainedFields:保留属性字段 + * 步骤: + * 1) 调用基础 adapterOut 生成 BPMN JSON; + * 2) 使用 lfJson2Xml 转为合法的 XML 字符串(包含属性与文本的转义)。 + */ adapterXmlOut = (data, retainedFields?: string[]) => { const outData = this.adapterOut(data, retainedFields) return lfJson2Xml(outData) diff --git a/packages/extension/src/bpmn-adapter/json2xml.ts b/packages/extension/src/bpmn-adapter/json2xml.ts index e2536b0c4..d1fbb8116 100644 --- a/packages/extension/src/bpmn-adapter/json2xml.ts +++ b/packages/extension/src/bpmn-adapter/json2xml.ts @@ -26,6 +26,17 @@ function handleAttributes(o: any) { return t } +function escapeXml(text: string) { + if (text == null) return '' + return text + .toString() + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, ''') +} + function getAttributes(obj: any) { let tmp = obj try { @@ -35,7 +46,8 @@ function getAttributes(obj: any) { } catch (error) { tmp = JSON.stringify(handleAttributes(obj)).replace(/"/g, "'") } - return tmp + // 确保属性值中的特殊字符被正确转义 + return escapeXml(String(tmp)) } const tn = '\t\n' @@ -51,7 +63,7 @@ function toXml(obj: string | any[] | Object, name: string, depth: number) { let str = '' if (name === '#text') { - return tn + frontSpace + obj + return tn + frontSpace + escapeXml(String(obj)) } else if (name === '#cdata-section') { return tn + frontSpace + '' } else if (name === '#comment') { @@ -78,7 +90,7 @@ function toXml(obj: string | any[] | Object, name: string, depth: number) { attributes + (children !== '' ? `>${children}${tn + frontSpace}` : ' />') } else { - str += tn + frontSpace + `<${name}>${obj.toString()}` + str += tn + frontSpace + `<${name}>${escapeXml(String(obj))}` } } diff --git a/packages/extension/src/bpmn-adapter/xml2json.ts b/packages/extension/src/bpmn-adapter/xml2json.ts index 597f720b2..9cbb6a60d 100644 --- a/packages/extension/src/bpmn-adapter/xml2json.ts +++ b/packages/extension/src/bpmn-adapter/xml2json.ts @@ -283,11 +283,14 @@ XML.ObjTree.prototype.scalar_to_xml = function (name, text) { // method: xml_escape( text ) XML.ObjTree.prototype.xml_escape = function (text) { + if (text == null) return '' return text - .replace(/&/g, '&') - .replace(//g, '>') - .replace(/"/g, '"') + .toString() + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, ''') } /* diff --git a/packages/extension/src/tools/proximity-connect/index.ts b/packages/extension/src/tools/proximity-connect/index.ts index 709ffa674..c82fb9164 100644 --- a/packages/extension/src/tools/proximity-connect/index.ts +++ b/packages/extension/src/tools/proximity-connect/index.ts @@ -15,11 +15,19 @@ export type ProximityConnectProps = { distance: number reverseDirection: boolean virtualEdgeStyle: Record + /** + * proximityConnect 类型: + * - 'node': 节点-节点连接 + * - 'anchor': 锚点-锚点连接 + * - 'default': 节点-锚点连接 + */ + type: 'node' | 'anchor' | 'default' } export class ProximityConnect { static pluginName = 'proximityConnect' enable: boolean = true + type: 'node' | 'anchor' | 'default' = 'default' lf: LogicFlow // lf实例 closestNode?: BaseNodeModel // 当前距离最近的节点 currentDistance: number = Infinity // 当前间距 @@ -52,6 +60,7 @@ export class ProximityConnect { addEventListeners() { // 节点开始拖拽事件 this.lf.graphModel.eventCenter.on('node:dragstart', ({ data }) => { + if (this.type === 'anchor') return if (!this.enable) return const { graphModel } = this.lf const { id } = data @@ -59,13 +68,14 @@ export class ProximityConnect { }) // 节点拖拽事件 this.lf.graphModel.eventCenter.on('node:drag', () => { + if (this.type === 'anchor') return this.handleNodeDrag() }) // 锚点开始拖拽事件 this.lf.graphModel.eventCenter.on( 'anchor:dragstart', ({ data, nodeModel }) => { - if (!this.enable) return + if (!this.enable || this.type === 'node') return this.currentNode = nodeModel this.currentAnchor = data }, @@ -74,18 +84,18 @@ export class ProximityConnect { this.lf.graphModel.eventCenter.on( 'anchor:drag', ({ e: { clientX, clientY } }) => { - if (!this.enable) return + if (!this.enable || this.type === 'node') return this.handleAnchorDrag(clientX, clientY) }, ) // 节点、锚点拖拽结束事件 this.lf.graphModel.eventCenter.on('node:drop', () => { - if (!this.enable) return + if (!this.enable || this.type === 'anchor') return this.handleDrop() }) // 锚点拖拽需要单独判断一下当前拖拽终点是否在某个锚点上,如果是,就不触发插件的连线,以免出现创建了两条连线的问题,表现见 issue 2140 this.lf.graphModel.eventCenter.on('anchor:dragend', ({ e, edgeModel }) => { - if (!this.enable) return + if (!this.enable || this.type === 'node') return const { canvasOverlayPosition: { x: eventX, y: eventY }, } = this.lf.graphModel.getPointByClient({ diff --git a/packages/extension/src/tools/snapshot/index.ts b/packages/extension/src/tools/snapshot/index.ts index acb43b97d..afaa3ec23 100644 --- a/packages/extension/src/tools/snapshot/index.ts +++ b/packages/extension/src/tools/snapshot/index.ts @@ -35,6 +35,14 @@ export type ToImageOptions = { * - `true`:只导出画面区域内的可见元素 */ partial?: boolean + /** + * 导出图片时的安全系数,用于确保导出的图片能够容纳所有元素,默认值为 1.1 + */ + safetyFactor?: number + /** + * 导出图片时的安全边距,用于确保导出的图片能够容纳所有元素,默认值为 40 + */ + safetyMargin?: number } // Blob | base64 @@ -381,7 +389,7 @@ export class Snapshot { // 计算实际宽高,考虑缩放因素 // 在宽画布情况下,getBoundingClientRect可能无法获取到所有元素的边界 // 因此我们添加一个安全系数来确保能够容纳所有元素 - const safetyFactor = 1.1 // 安全系数,增加20%的空间 + const safetyFactor = toImageOptions.safetyFactor || 1.1 // 安全系数,增加10%的空间 const actualWidth = (bbox.width / SCALE_X) * safetyFactor const actualHeight = (bbox.height / SCALE_Y) * safetyFactor @@ -394,7 +402,7 @@ export class Snapshot { // 宽高值 默认加padding 40,保证图形不会紧贴着下载图片 // 为宽画布添加额外的安全边距,确保不会裁剪 - const safetyMargin = 40 // 额外的安全边距 + const safetyMargin = toImageOptions.safetyMargin || 40 // 额外的安全边距 // 获取当前浏览器类型,不同浏览器对canvas的限制不同 const { maxCanvasDimension, otherMaxCanvasDimension } = diff --git a/sites/docs/docs/tutorial/extension/dynamic-group.zh.md b/sites/docs/docs/tutorial/extension/dynamic-group.zh.md index 551351e4d..c1577c7ef 100644 --- a/sites/docs/docs/tutorial/extension/dynamic-group.zh.md +++ b/sites/docs/docs/tutorial/extension/dynamic-group.zh.md @@ -6,7 +6,6 @@ group: title: 动态分组 (DynamicGroup) order: 8 toc: content -tag: 新插件 --- LogicFlow 支持动态分组。动态分组是 LogicFlow 内置的自定义节点, 是 Group 分组的升级版本(因为我们内置了 Node Resize 功能,且 Group 分组的功能命名不够规范,所以我们推出了升级版的 DynamicGroup 节点)。我们会持续在该插件中做能力增强,欢迎大家一起参与共建。 diff --git a/sites/docs/docs/tutorial/extension/highlight.zh.md b/sites/docs/docs/tutorial/extension/highlight.zh.md index 9e32be098..063b9f10d 100644 --- a/sites/docs/docs/tutorial/extension/highlight.zh.md +++ b/sites/docs/docs/tutorial/extension/highlight.zh.md @@ -6,7 +6,6 @@ group: title: 高亮 (Highlight) order: 12 toc: content -tag: 优化 --- 当一张画布上的元素逐渐增多时,快速分辨出当前获焦的节点是谁,都连了几条线,连到了哪些节点上的难度也会逐渐增大,为了方便用户快速看清当前获焦节点的关联信息,我们提供了高亮功能。 @@ -22,10 +21,10 @@ LogicFlow.use(Highlight) ``` ## 配置项 -| 字段 | 类型 | 作用 | 是否必须 | 描述 | -|--------|---------|-----------------|------|--------------------------------------------------------------------------------------------------------------------| -| mode | string | 高亮类型,用于控制高亮展示效果 | | 该配置项支持传入三个值:
1. 'single': 高亮当前节点/边
2. 'path': 高亮当前节点/边所在路径上所有的边和节点
3. 'neighbour': 高亮当前节点/边直接关联的所有边和节点 | -| enable | boolean | 是否启用高亮 | | 该配置项用于控制是否展示高亮效果 | +| 字段 | 类型 | 作用 | 是否必须 | 描述 | +| ------ | ------- | ------------------------------ | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| mode | string | 高亮类型,用于控制高亮展示效果 | | 该配置项支持传入三个值:
1. 'single': 高亮当前节点/边
2. 'path': 高亮当前节点/边所在路径上所有的边和节点
3. 'neighbour': 高亮当前节点/边直接关联的所有边和节点 | +| enable | boolean | 是否启用高亮 | | 该配置项用于控制是否展示高亮效果 | ## 默认状态 默认情况下,高亮插件处于启用状态,使用的是 高亮当前节点/边所在路径上所有的边和节点 模式,鼠标移入节点内部就会生效。 diff --git a/sites/docs/docs/tutorial/extension/label.en.md b/sites/docs/docs/tutorial/extension/label.en.md index e3b6d936c..45307a6e8 100644 --- a/sites/docs/docs/tutorial/extension/label.en.md +++ b/sites/docs/docs/tutorial/extension/label.en.md @@ -6,7 +6,6 @@ group: title: Label order: 7 toc: content -tag: New --- LogicFlow provides built-in node text and text editing capabilities, but sometimes we need richer text content, such as multi-line text or rich text. In such cases, you can use the `Label` plugin. @@ -44,12 +43,12 @@ const lf = new LogicFlow({ Each feature in the menu can be represented by a single configuration. The specific fields are as follows: -| Field | Type | Default Value | Required | Description | -|---------------------|-----------------------------------------------------------|---------------|----------|--------------------------------------------------| -| isMultiple | boolean | `true` | | Whether a node or edge can have multiple Labels. | -| maxCount | number | `Infinity` | | When multiple Labels are allowed, the maximum number of Labels. | -| labelWidth | number | - | | The width of each Label, can be uniformly set and used with textOverflowMode. | -| textOverflowMode | 'ellipsis' \| 'wrap' \| 'clip' \| 'nowrap' \| 'default' | `default` | | Text overflow display mode, consistent with CSS configuration. | +| Field | Type | Default Value | Required | Description | +| ---------------- | ------------------------------------------------------- | ------------- | -------- | ----------------------------------------------------------------------------- | +| isMultiple | boolean | `true` | | Whether a node or edge can have multiple Labels. | +| maxCount | number | `Infinity` | | When multiple Labels are allowed, the maximum number of Labels. | +| labelWidth | number | - | | The width of each Label, can be uniformly set and used with textOverflowMode. | +| textOverflowMode | 'ellipsis' \| 'wrap' \| 'clip' \| 'nowrap' \| 'default' | `default` | | Text overflow display mode, consistent with CSS configuration. | ### Element-Level diff --git a/sites/docs/docs/tutorial/extension/label.zh.md b/sites/docs/docs/tutorial/extension/label.zh.md index 9fb3071c5..3d24513d8 100644 --- a/sites/docs/docs/tutorial/extension/label.zh.md +++ b/sites/docs/docs/tutorial/extension/label.zh.md @@ -6,7 +6,6 @@ group: title: 富文本标签 (Label) order: 7 toc: content -tag: 新插件 --- LogicFlow diff --git a/sites/docs/docs/tutorial/extension/layout.zh.md b/sites/docs/docs/tutorial/extension/layout.zh.md index 728be622c..9668ace37 100644 --- a/sites/docs/docs/tutorial/extension/layout.zh.md +++ b/sites/docs/docs/tutorial/extension/layout.zh.md @@ -6,7 +6,6 @@ group: title: 自动布局 (Layout) order: 7 toc: content -tag: 增强 --- 在复杂的流程图中,手动排列节点和边缘可能既耗时又难以保持整洁。LogicFlow 提供了自动布局插件,能够自动计算节点位置和边的路径,使图表呈现出结构化且美观的效果。 @@ -95,18 +94,18 @@ lf.extension.dagre.layout({ 通过配置不同的选项,您可以自定义布局的外观和行为。以下是支持的选项: -| 参数名 | 类型 | 默认值 | 说明 | -|-------|-----|-------|------| -| rankdir | string | 'LR' | 布局方向,'LR'(左到右), 'TB'(上到下), 'BT'(下到上), 'RL'(右到左) | -| align | string | 'UL' | 节点对齐方式,'UL'(上左), 'UR'(上右), 'DL'(下左), 'DR'(下右) | -| nodesep | number | 100 | 节点间的水平间距(像素) | -| ranksep | number | 150 | 层级间的垂直间距(像素) | -| marginx | number | 120 | 图的水平边距(像素) | -| marginy | number | 120 | 图的垂直边距(像素) | -| ranker | string | 'tight-tree' | 排名算法,'network-simplex', 'tight-tree', 'longest-path' | -| edgesep | number | 10 | 边之间的水平间距(像素) | -| acyclicer | string | undefined | 如果设置为'greedy',使用贪心算法查找反馈弧集,用于使图变为无环图 | -| isDefaultAnchor | boolean | false | 是否使用默认锚点:true表示会自动调整连线锚点,根据布局方向计算边的路径 | +| 参数名 | 类型 | 默认值 | 说明 | +| --------------- | ------- | ------------ | ---------------------------------------------------------------------- | +| rankdir | string | 'LR' | 布局方向,'LR'(左到右), 'TB'(上到下), 'BT'(下到上), 'RL'(右到左) | +| align | string | 'UL' | 节点对齐方式,'UL'(上左), 'UR'(上右), 'DL'(下左), 'DR'(下右) | +| nodesep | number | 100 | 节点间的水平间距(像素) | +| ranksep | number | 150 | 层级间的垂直间距(像素) | +| marginx | number | 120 | 图的水平边距(像素) | +| marginy | number | 120 | 图的垂直边距(像素) | +| ranker | string | 'tight-tree' | 排名算法,'network-simplex', 'tight-tree', 'longest-path' | +| edgesep | number | 10 | 边之间的水平间距(像素) | +| acyclicer | string | undefined | 如果设置为'greedy',使用贪心算法查找反馈弧集,用于使图变为无环图 | +| isDefaultAnchor | boolean | false | 是否使用默认锚点:true表示会自动调整连线锚点,根据布局方向计算边的路径 | ## 高级功能 diff --git a/sites/docs/docs/tutorial/extension/minimap.en.md b/sites/docs/docs/tutorial/extension/minimap.en.md index 95049c576..8d3706301 100644 --- a/sites/docs/docs/tutorial/extension/minimap.en.md +++ b/sites/docs/docs/tutorial/extension/minimap.en.md @@ -6,7 +6,6 @@ group: title: MiniMap order: 4 toc: content -tag: Optimization ---