From ffb4d8c2c6bfdc2e46b0ce685cd3dd8c904ebfc3 Mon Sep 17 00:00:00 2001 From: ModestFun <61576426+ModestFun@users.noreply.github.com> Date: Fri, 3 Jan 2025 15:57:57 +0800 Subject: [PATCH] Feat/swim bg (#113) * :coffin: feat: tina flow demo * :sparkles: feat: add swim lane * :sparkles: feat: add dispatch * :bug: fix: ci * :bug: fix: coverage * :sparkles: feat: type --------- Co-authored-by: jiangchu --- .github/workflows/test.yml | 4 +- .../demos/tinaFlow/comp/box/index.less | 126 ++++++++++++ .../demos/tinaFlow/comp/box/index.tsx | 143 +++++++++++++ .../demos/tinaFlow/comp/comp/index.tsx | 45 +++++ docs/caseShow/demos/tinaFlow/comp/ctx.tsx | 76 +++++++ .../demos/tinaFlow/comp/group/index.less | 48 +++++ .../demos/tinaFlow/comp/group/index.tsx | 57 ++++++ .../demos/tinaFlow/comp/icon/index.tsx | 6 + docs/caseShow/demos/tinaFlow/index.tsx | 191 ++++++++++++++++++ docs/guide/demos/flowViewIntro/autoFlow.tsx | 2 +- .../data/{data2.ts => data2.tsx} | 2 + src/Background/components/SwimBg.tsx | 71 +++++++ src/Background/demos/swim.tsx | 68 +++++++ src/Background/index.zh-CN.md | 55 +++-- src/FlowEditor/store/slices/edgesSlice.ts | 11 +- .../store/slices/generalActionSlice.ts | 5 + src/FlowEditor/store/slices/nodesSlice.ts | 11 +- src/FlowView/helper.tsx | 2 + src/Input/index.tsx | 2 +- src/index.ts | 12 +- testDagre.js | 142 +++++++++++++ 21 files changed, 1054 insertions(+), 25 deletions(-) create mode 100644 docs/caseShow/demos/tinaFlow/comp/box/index.less create mode 100644 docs/caseShow/demos/tinaFlow/comp/box/index.tsx create mode 100644 docs/caseShow/demos/tinaFlow/comp/comp/index.tsx create mode 100644 docs/caseShow/demos/tinaFlow/comp/ctx.tsx create mode 100644 docs/caseShow/demos/tinaFlow/comp/group/index.less create mode 100644 docs/caseShow/demos/tinaFlow/comp/group/index.tsx create mode 100644 docs/caseShow/demos/tinaFlow/comp/icon/index.tsx create mode 100644 docs/caseShow/demos/tinaFlow/index.tsx rename docs/guide/demos/flowViewIntro/data/{data2.ts => data2.tsx} (93%) create mode 100644 src/Background/components/SwimBg.tsx create mode 100644 src/Background/demos/swim.tsx create mode 100644 testDagre.js diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 3edee65..701133a 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -26,5 +26,5 @@ jobs: - name: Test and coverage run: pnpm run test:coverage - - name: Upload coverage to Codecov - uses: codecov/codecov-action@v3 + # - name: Upload coverage to Codecov + # uses: codecov/codecov-action@v3 diff --git a/docs/caseShow/demos/tinaFlow/comp/box/index.less b/docs/caseShow/demos/tinaFlow/comp/box/index.less new file mode 100644 index 0000000..0bb6a58 --- /dev/null +++ b/docs/caseShow/demos/tinaFlow/comp/box/index.less @@ -0,0 +1,126 @@ +.box { + // width: 600px; + // height: 500px; + // height: 100%; + // border: 1px solid #666; + position: relative; + // background-color: cyan; + transition: width 0.2s, height 0.2s; + &-move { + transition: width 0.05s, height 0.05s; + // 不能选中 + user-select: none; + } + .box-holder { + position: absolute; + &-mousedown { + background-color: rgba(27, 110, 197, 0.7); + } + &:hover { + background-color: rgba(27, 110, 197, 0.7); + } + &-left, &-right { + top: 0; + bottom: 0; + width: 8px; + &:hover { + cursor: col-resize; + } + } + &-left { + left: 0; + } + &-right { + right: 0; + } + + &-top, &-bottom { + left: 0; + right: 0; + height: 8px; + &:hover { + cursor: row-resize; + } + } + &-top { + top: 0; + } + &-bottom { + bottom: 0; + } + } + + .expander { + z-index: 99; + position: absolute; + opacity: 0.7; + border: 12px solid transparent; + // width: 12px; + // height: 64px; + color: rgba(0, 10, 26, .45); + cursor: pointer; + transition: opacity 0.3s; + box-sizing: border-box; + svg { + position: absolute; + top: 50%; + left: 0; + transform: translate(-100%, -50%); + } + &:hover { + opacity: 1; + } + &-left, &-right { + top: 50%; + width: 12px; + height: 64px; + svg { + top: 50%; + } + } + &-left { + left: 0; + transform: translate(-100%, -50%); + border-right: 12px solid #d1d0d0; + svg { + right: 25%; + transform: translate(0, -50%); + } + } + &-right { + right: 0; + transform: translate(100%, -50%); + border-left: 12px solid #d1d0d0; + svg { + left: 0; + transform: translate(-100%, -50%); + } + } + &-top, &-bottom { + left: 50%; + width: 64px; + height: 12px; + svg { + left: 50%; + } + } + &-top { + top: 0; + transform: translate(-50%, -100%); + border-bottom: 12px solid #d1d0d0; + svg { + bottom: 25%; + transform: translate(-50%, -11%) rotate(-90deg); + } + } + &-bottom { + bottom: 0; + transform: translate(-50%, 100%); + border-top: 12px solid #d1d0d0; + svg { + top: 25%; + transform: translate(-50%, -89%) rotate(90deg); + } + } + } +} diff --git a/docs/caseShow/demos/tinaFlow/comp/box/index.tsx b/docs/caseShow/demos/tinaFlow/comp/box/index.tsx new file mode 100644 index 0000000..350bd87 --- /dev/null +++ b/docs/caseShow/demos/tinaFlow/comp/box/index.tsx @@ -0,0 +1,143 @@ +import React, { FC, useEffect, useRef, useState } from 'react'; +import ReactDOM from 'react-dom'; +import './index.less'; + +export const Box: FC<{ + style?: React.CSSProperties; + className?: string; + direction: 'top' | 'bottom' | 'left' | 'right'; + // 是否展示收起展开 + showExpand?: boolean; + children?: React.ReactNode; + onResizeEnd?: () => void; +}> = ({ className, direction, showExpand, style: _style, children }) => { + const mouseDownState = useState(false); + const isExpandState = useState(false); + + const mouseDownRef = useRef({ + isMouseDown: false, + x: 0, + y: 0, + width: 0, + height: 0, + }); + + const boxRef = useRef(null); + + const handleMouseDown = (event: React.MouseEvent) => { + mouseDownState[1](true); + mouseDownRef.current = { + isMouseDown: true, + x: event.clientX, + y: event.clientY, + width: boxRef.current!.offsetWidth, + height: boxRef.current!.offsetHeight, + }; + document.documentElement.style.cursor = + direction === 'bottom' || direction === 'top' ? 'row-resize' : 'col-resize'; + }; + + useEffect(() => { + const handleMouseup = () => { + mouseDownState[1](false); + mouseDownRef.current.isMouseDown = false; + document.documentElement.style.cursor = 'auto'; + }; + + const handleMousemove = ({ clientX, clientY }: React.MouseEvent) => { + requestAnimationFrame(() => { + if (!mouseDownRef.current.isMouseDown || !boxRef.current) return; + const moveHorizontalDistance = clientX - mouseDownRef.current.x; + const moveVerticalDistance = clientY - mouseDownRef.current.y; + if (direction === 'right') { + boxRef.current.style.width = mouseDownRef.current.width + moveHorizontalDistance + 'px'; + } + if (direction === 'left') { + boxRef.current.style.width = mouseDownRef.current.width - moveHorizontalDistance + 'px'; + } + if (direction === 'top') { + boxRef.current.style.height = mouseDownRef.current.height - moveVerticalDistance + 'px'; + } + if (direction === 'bottom') { + boxRef.current.style.height = mouseDownRef.current.height + moveVerticalDistance + 'px'; + } + }); + }; + + window.addEventListener('mouseup', handleMouseup); + window.addEventListener('mousemove', handleMousemove as any, false); + + return () => { + window.removeEventListener('mouseup', handleMouseup); + window.removeEventListener('mousemove', handleMousemove as any, false); + }; + }, []); + + const handleExpandToggle = () => { + const isHorizontal = direction === 'right' || direction === 'left'; + if (!isExpandState[0]) { + mouseDownRef.current.width = boxRef.current!.offsetWidth; + mouseDownRef.current.height = boxRef.current!.offsetHeight; + boxRef.current!.style[isHorizontal ? 'width' : 'height'] = '0px'; + } else { + boxRef.current!.style[isHorizontal ? 'width' : 'height'] = + mouseDownRef.current[isHorizontal ? 'width' : 'height'] + 'px'; + } + isExpandState[1](!isExpandState[0]); + }; + + return ( +
+ {!isExpandState[0] && ( +
+ )} +
{children}
+ {showExpand && ( +
+
+ + + +
+
+ )} + {mouseDownState[0] && + ReactDOM.createPortal( +
, + document.body, + )} +
+ ); +}; diff --git a/docs/caseShow/demos/tinaFlow/comp/comp/index.tsx b/docs/caseShow/demos/tinaFlow/comp/comp/index.tsx new file mode 100644 index 0000000..4e7e7a6 --- /dev/null +++ b/docs/caseShow/demos/tinaFlow/comp/comp/index.tsx @@ -0,0 +1,45 @@ +import { Handle, Position, useFlowViewer } from '@ant-design/pro-flow'; +import { FC, useState } from 'react'; +import { IconFont } from '../icon'; + +interface CompNodeProps { + className?: string; +} + +export const CompNode: FC = function CompNode(props) { + const s = useState({ height: '100px', width: '200px' }); + // const { updateNodeMeta } = useFlowEditor(); + const { selectNode, selectEdges, selectNodes, zoomToNode, fullScreen, instance } = + useFlowViewer(); + + // @ts-ignore + window.instance = instance; + return ( +
+ + compcompcompcomp + { + if (s[0].width === '200px') { + s[1]({ height: '200px', width: '250px' }); + props?.data?.onResize(250, 200); + } else { + s[1]({ height: '100px', width: '200px' }); + props?.data?.onResize(200, 100); + } + console.log('props: ', props); + // updateNodeMeta(props.id, "height", 200); + // updateNodeMeta(props.id, "width", 250); + }} + /> +
+ ); +}; diff --git a/docs/caseShow/demos/tinaFlow/comp/ctx.tsx b/docs/caseShow/demos/tinaFlow/comp/ctx.tsx new file mode 100644 index 0000000..1bf7578 --- /dev/null +++ b/docs/caseShow/demos/tinaFlow/comp/ctx.tsx @@ -0,0 +1,76 @@ +import { FlowViewEdge, FlowViewNode, useFlowViewer } from '@ant-design/pro-flow'; +import { FC, createContext, useCallback, useContext, useState } from 'react'; + +interface ICtx { + nodes: FlowViewNode[]; + edges: FlowViewEdge[]; + init: (nodes: FlowViewNode[], edges: FlowViewEdge[]) => void; + updateNodes: ( + items: [string, Partial | ((node: FlowViewNode) => FlowViewNode)][], + ) => void; + removeNodes: (ids: string[]) => void; +} + +const Ctx = createContext({ + nodes: [], + edges: [], + init: () => {}, + updateNodes: () => {}, + removeNodes: () => {}, +}); + +export const MindProvider: FC<{ children: React.ReactNode }> = ({ children }) => { + const nodesState = useState([]); + const edgesState = useState([]); + + const updateNodes = useCallback((items) => { + nodesState[1]((nodes) => { + const _nodes = [...nodes]; + items.forEach(([id, item]) => { + const idx = _nodes.findIndex((n) => n.id === id); + const node = _nodes[idx]; + if (node) { + if (typeof item === 'function') { + _nodes[idx] = { ...node, ...item(node) }; + } else { + _nodes[idx] = { ...node, ...item }; + } + } + }); + // @ts-ignore + window.nodes = _nodes; + return _nodes; + }); + }, []); + + const removeNodes = useCallback((ids) => { + nodesState[1]((nodes) => { + return nodes.filter((node) => !ids.includes(node.id)); + }); + edgesState[1]((edges) => { + return edges.filter((edge) => !ids.includes(edge.source) && !ids.includes(edge.target)); + }); + }, []); + + const init = useCallback((nodes, edges) => { + nodesState[1](nodes); + edgesState[1](edges); + }, []); + + return ( + + {children} + + ); +}; + +export function useMind() { + const { selectNode, selectEdges, selectNodes, zoomToNode, fullScreen, instance } = + useFlowViewer(); + + // @ts-ignore + window.instance = instance; + return useContext(Ctx); +} diff --git a/docs/caseShow/demos/tinaFlow/comp/group/index.less b/docs/caseShow/demos/tinaFlow/comp/group/index.less new file mode 100644 index 0000000..7e29a13 --- /dev/null +++ b/docs/caseShow/demos/tinaFlow/comp/group/index.less @@ -0,0 +1,48 @@ +@w: 155px; + +.group { + background-color: #fff; + border-radius: 8px; + box-shadow: 0 4px 6px -2px rgba(25, 15, 15, 0.05), 0 0 1px 0 rgba(0, 0, 0, 0.08); + padding: 0 12px 16px; + width: @w * 2 + 8px + 12px * 2; + box-sizing: border-box; + .header { + display: flex; + padding: 8px 0; + img { + width: 16px; + vertical-align: middle; + margin-right: 8px; + } + .title { + flex: 1 1 auto; + } + } + + .body { + display: flex; + flex-wrap: wrap; + gap: 8px; + .item { + width: @w; + border: 1px solid #eee; + box-sizing: border-box; + border-radius: 8px; + box-shadow: 0 4px 6px -2px rgba(25, 15, 15, 0.05), 0 0 1px 0 rgba(0, 0, 0, 0.08); + padding: 12px 8px; + .itemTitle { + font-size: 14px; + } + .itemDesc { + color: #666; + font-size: 12px; + } + } + } +} + +.handle { + top: 41.5px; + left: -9px; +} diff --git a/docs/caseShow/demos/tinaFlow/comp/group/index.tsx b/docs/caseShow/demos/tinaFlow/comp/group/index.tsx new file mode 100644 index 0000000..f378acb --- /dev/null +++ b/docs/caseShow/demos/tinaFlow/comp/group/index.tsx @@ -0,0 +1,57 @@ +import { Handle, NodeProps, Position } from '@ant-design/pro-flow'; +import React, { FC, useEffect } from 'react'; + +import { useMind } from '../ctx'; +import { IconFont } from '../icon'; +import './index.less'; + +interface GroupProps extends NodeProps { + className?: string; +} + +export const Group: FC = function Group(props) { + const { updateNodes } = useMind(); + + const [count, setCount] = React.useState(props.data?.count); + + useEffect(() => { + const row = Math.ceil(count / 2); + updateNodes([[props.id, { height: 38.5 + row * 63 + (row - 1) * 8 + 16, width: 342 }]]); + }, [count]); + + return ( +
+ + +
+ +
bizData
+
+ { + setCount((count) => count + 1); + }} + /> +
+
+ +
+ {new Array(count).fill('').map((item, i) => ( +
+
SaveMoneyContainer
+
直塞发奖弹窗
+
+ ))} +
+
+ ); +}; diff --git a/docs/caseShow/demos/tinaFlow/comp/icon/index.tsx b/docs/caseShow/demos/tinaFlow/comp/icon/index.tsx new file mode 100644 index 0000000..1560ec4 --- /dev/null +++ b/docs/caseShow/demos/tinaFlow/comp/icon/index.tsx @@ -0,0 +1,6 @@ +import { createFromIconfontCN } from '@ant-design/icons'; + +// 使用你在 iconfont.cn 上的项目URL +export const IconFont = createFromIconfontCN({ + scriptUrl: '//at.alicdn.com/t/a/font_4664976_ncmcql8595.js', // 示例URL +}); diff --git a/docs/caseShow/demos/tinaFlow/index.tsx b/docs/caseShow/demos/tinaFlow/index.tsx new file mode 100644 index 0000000..5d70174 --- /dev/null +++ b/docs/caseShow/demos/tinaFlow/index.tsx @@ -0,0 +1,191 @@ +import { FlowView, useFlowViewer } from '@ant-design/pro-flow'; +import { FC, useEffect, useState } from 'react'; + +// import "./flow.less"; +import { TreeDataNode } from 'antd'; +import { CompNode } from './comp/comp'; +import { MindProvider, useMind } from './comp/ctx'; +import { Group } from './comp/group'; +import { IconFont } from './comp/icon'; + +interface FlowProps { + className?: string; +} + +const nodeTypes = { + CompNode, + Group, +}; + +export const Flow: FC = function Flow(props) { + const { selectNode, selectEdges, selectNodes, zoomToNode, fullScreen, instance } = + useFlowViewer(); + + // @ts-ignore + window.instance = instance; + // instance?.setViewport + + const s = useState({ width: 200, height: 100 }); + + // @ts-ignore + window.ss = s[1]; + + const { nodes, edges, init } = useMind(); + + useEffect(() => { + init(_nodes, _edges); + }, []); + + return ( +
+ zoomToNode(node!.id, 1000)} + /> +
+ ); +}; + +export default () => { + return ( + + + + ); +}; + +const treeData: TreeDataNode[] = [ + { + title: 'parent 1', + key: '0-0', + icon: , + children: [ + { + title: 'parent 1-0', + key: '0-0-0', + icon: , + children: [ + { title: 'leaf', key: '0-0-0-0', icon: }, + { + title: ( + <> +
multiple line title
+
multiple line title
+ + ), + key: '0-0-0-1', + icon: , + }, + { title: 'leaf', key: '0-0-0-2', icon: }, + ], + }, + { + title: 'parent 1-1', + key: '0-0-1', + icon: , + children: [{ title: 'leaf', key: '0-0-1-0', icon: }], + }, + { + title: 'parent 1-2', + key: '0-0-2', + icon: , + children: [ + { title: 'leaf', key: '0-0-2-0', icon: }, + { + title: 'leaf', + key: '0-0-2-1', + icon: , + // switcherIcon: , + }, + ], + }, + ], + }, + { + title: 'parent 2', + key: '0-1', + icon: , + children: [ + { + title: 'parent 2-0', + key: '0-1-0', + icon: , + children: [ + { title: 'leaf', key: '0-1-0-0', icon: }, + { title: 'leaf', key: '0-1-0-1', icon: }, + ], + }, + ], + }, +]; + +const _nodes: any[] = [ + { + id: 'b1', + data: { + title: 'SaveMoneyContainer', + description: '直塞发奖弹窗', + logo: 'https://mdn.alipayobjects.com/huamei_fcqe9k/afts/img/A*j8ICSbNgZ3wAAAAAAAAAAAAADtOFAQ/original', + titleSlot: { + type: 'right', + value: aaa, + }, + }, + }, + { + id: 'b11', + type: 'Group', + width: 200, + data: { + count: 10, + }, + }, + { + id: 'b2', + type: 'Group', + width: 234, + data: { + count: 2, + }, + }, + // { + // id: "b2", + // type: "BasicNodeGroup", + // label: "bizData", + // title: "bizData", + // selectType: "SELECT", + // data: [ + // "canSetFirst", + // "eventModalVisible", + // "quotaPopupData", + // "quotaPopupData1", + // "quotaPopupData2", + // "quotaPopupData3", + // "quotaPopupData4", + // ].map((item) => ({ + // id: item, + // data: { + // title: item, + // description: "bizData 描述描述", + // logo: "https://mdn.alipayobjects.com/huamei_fcqe9k/afts/img/A*OavdQKLWtjAAAAAAAAAAAAAADtOFAQ/original", + // }, + // })), + // }, +]; +const _edges: any[] = [ + { + id: 'b111', + source: 'b1', + target: 'b11', + }, + { + id: 'b12', + source: 'b1', + target: 'b2', + }, +]; diff --git a/docs/guide/demos/flowViewIntro/autoFlow.tsx b/docs/guide/demos/flowViewIntro/autoFlow.tsx index a994b74..0c64bdb 100644 --- a/docs/guide/demos/flowViewIntro/autoFlow.tsx +++ b/docs/guide/demos/flowViewIntro/autoFlow.tsx @@ -4,7 +4,7 @@ */ import { FlowView } from '@ant-design/pro-flow'; import useStyles from './css/index.style'; -import { edges, nodes } from './data/data2'; +import { edges, nodes } from './data/data2.tsx'; function App() { const { styles } = useStyles(); diff --git a/docs/guide/demos/flowViewIntro/data/data2.ts b/docs/guide/demos/flowViewIntro/data/data2.tsx similarity index 93% rename from docs/guide/demos/flowViewIntro/data/data2.ts rename to docs/guide/demos/flowViewIntro/data/data2.tsx index 04ed13b..4732623 100644 --- a/docs/guide/demos/flowViewIntro/data/data2.ts +++ b/docs/guide/demos/flowViewIntro/data/data2.tsx @@ -29,11 +29,13 @@ export const edges = [ id: 'a1-a2', source: 'a1', target: 'a2', + label:
1231123
, }, { id: 'a1-a3', source: 'a1', target: 'a3', type: 'radius', + label:
1231123
, }, ]; diff --git a/src/Background/components/SwimBg.tsx b/src/Background/components/SwimBg.tsx new file mode 100644 index 0000000..bc7b2ce --- /dev/null +++ b/src/Background/components/SwimBg.tsx @@ -0,0 +1,71 @@ +import { createStyles } from 'antd-style'; + +const useStyles = createStyles(({ css }) => ({ + container: css` + width: 100%; + height: 100%; + display: flex; + /* flex-direction: column; */ + `, + label: css` + text-align: center; + line-height: 40px; + height: 40px; + border-bottom: 1px solid #cbcdce; + color: '#A8AAAE'; + `, +})); + +export interface SwimlaneBackgroundProps { + lanes: SwimLaneProps[]; + className?: string; + style?: React.CSSProperties; +} + +export interface SwimLaneProps { + id: string; + label: string; + labelColor?: string; + backgroundColor?: string; + width?: string; + style?: React.CSSProperties; +} + +const SwimlaneBackground = (props: SwimlaneBackgroundProps) => { + const { lanes, className: clm, style } = props; + const { styles } = useStyles(); + + return ( +
+ {lanes.map((lane) => ( +
+
+ {lane.label} +
+
+ ))} +
+ ); +}; + +export default SwimlaneBackground; diff --git a/src/Background/demos/swim.tsx b/src/Background/demos/swim.tsx new file mode 100644 index 0000000..d55974d --- /dev/null +++ b/src/Background/demos/swim.tsx @@ -0,0 +1,68 @@ +/** + * compact: true + */ +import { FlowView, SwimLaneProps, SwimlaneBackground } from '@ant-design/pro-flow'; +import { createStyles } from 'antd-style'; +import { memo } from 'react'; + +const useStyles = createStyles(({ css }) => ({ + container: css` + width: 100%; + height: 600px; + `, +})); + +const BackgroundDemo = memo(() => { + const { styles } = useStyles(); + + return ( +
+ + + +
+ ); +}); + +export default BackgroundDemo; diff --git a/src/Background/index.zh-CN.md b/src/Background/index.zh-CN.md index d6077b0..a042298 100644 --- a/src/Background/index.zh-CN.md +++ b/src/Background/index.zh-CN.md @@ -14,28 +14,51 @@ description: +## Swim Background + + + ## Double Background ## APIs -| 属性名 | 类型 | 描述 | 默认值 | 必选 | -| --------- | --------------------------- | --------------------------------------- | --- | -- | -| id | `string` | 如果要显示多个背景,则需要 | - | - | -| className | `string` | 自定义类名 | - | - | -| variant | `BackgroundVariant` | 背景图案类型 | - | - | -| gap | `number \|[number, number]` | 模式之间的差距。您可以传递一个包含两个数字的数组来指定 x 间隙和 y 间隙。 | - | - | -| size | `number` | ” 点 “的半径或” 十字 “的尺寸 | - | - | -| lineWidth | `number` | ” 线 “或” 十字 “的宽度 | - | - | -| offset | `number` | 图案偏移 | - | - | -| color | `string` | 图案颜色 | - | - | -| style | `CSSProperties` | 样式属性 | - | - | +| 属性名 | 类型 | 描述 | 默认值 | 必选 | +| --------- | --------------------------- | ------------------------------------------------------------------------ | ------ | ---- | +| id | `string` | 如果要显示多个背景,则需要 | - | - | +| className | `string` | 自定义类名 | - | - | +| variant | `BackgroundVariant` | 背景图案类型 | - | - | +| gap | `number \|[number, number]` | 模式之间的差距。您可以传递一个包含两个数字的数组来指定 x 间隙和 y 间隙。 | - | - | +| size | `number` | ” 点 “的半径或” 十字 “的尺寸 | - | - | +| lineWidth | `number` | ” 线 “或” 十字 “的宽度 | - | - | +| offset | `number` | 图案偏移 | - | - | +| color | `string` | 图案颜色 | - | - | +| style | `CSSProperties` | 样式属性 | - | - | ### BackgroundVariant -| 属性名 | 类型 | 描述 | 默认值 | 必选 | -| ----- | -------- | -- | --- | -- | -| lines | `string` | 线 | - | - | -| dots | `string` | 点 | - | - | -| cross | `string` | 十字 | - | - | +| 属性名 | 类型 | 描述 | 默认值 | 必选 | +| ------ | -------- | ---- | ------ | ---- | +| lines | `string` | 线 | - | - | +| dots | `string` | 点 | - | - | +| cross | `string` | 十字 | - | - | + +### Swim Background Props + +| 属性名 | 类型 | 描述 | 默认值 | 必选 | +| --------- | --------------------- | -------- | ------ | ---- | +| lanes | `SwimLaneProps[]` | 泳道列表 | - | - | +| className | `string` | 类名 | - | - | +| style | `React.CSSProperties` | 样式 | - | - | + +### Swim Lane Props + +| 属性名 | 类型 | 描述 | 默认值 | 必选 | +| --------------- | --------------------- | ---------- | ------ | ---- | +| id | `string]` | id | - | - | +| label | `string` | 标签 | - | - | +| labelColor | `string` | 标签的背景 | - | - | +| backgroundColor | `string` | 泳道的背景 | - | - | +| width | `string` | 宽度 | - | - | +| style | `React.CSSProperties` | 样式 | - | - | diff --git a/src/FlowEditor/store/slices/edgesSlice.ts b/src/FlowEditor/store/slices/edgesSlice.ts index 21a0c78..956f552 100644 --- a/src/FlowEditor/store/slices/edgesSlice.ts +++ b/src/FlowEditor/store/slices/edgesSlice.ts @@ -10,7 +10,7 @@ import { EdgeDispatch, edgesReducer } from '../reducers/edge'; export interface PublicEdgesAction { dispatchEdges: (payload: EdgeDispatch, options?: ActionOptions) => void; addEdge: (edge: Edge) => void; - addEdges: (edges: Record, options?: ActionOptions) => void; + addEdges: (edges: Record | Edge[], options?: ActionOptions) => void; deleteEdge: (id: string) => void; deleteEdges: (ids: string[]) => void; updateEdge: (id: string, edge: Edge, options?: ActionOptions) => void; @@ -79,7 +79,14 @@ export const edgesSlice: StateCreator< }, addEdges: (edges, options) => { - get().dispatchEdges({ type: 'addEdges', edges: edges }, options); + const _edges = Array.isArray(edges) + ? edges.reduce((acc: Record, edge) => { + acc[edge.id] = edge; + return acc; + }, {}) + : edges; + + get().dispatchEdges({ type: 'addEdges', edges: _edges }, options); }, updateEdgesOnConnection: (connection) => { diff --git a/src/FlowEditor/store/slices/generalActionSlice.ts b/src/FlowEditor/store/slices/generalActionSlice.ts index e1e6d54..32bd32a 100644 --- a/src/FlowEditor/store/slices/generalActionSlice.ts +++ b/src/FlowEditor/store/slices/generalActionSlice.ts @@ -78,6 +78,7 @@ export const generalActionSlice: StateCreator< set({ selectedKeys }, false, payload); get().onSelectionChange?.(selectedKeys); }, + onElementSelectChange: (id, selected) => { if (selected) { get().selectElement(id); @@ -101,12 +102,14 @@ export const generalActionSlice: StateCreator< payload: { id }, }); }, + selectElements: (ids, expendSelection = false) => { get().internalUpdateSelection(expendSelection ? [...get().selectedKeys, ...ids] : ids, { type: 'selection/selectElements', payload: { ids }, }); }, + selectAll: () => { const nodes = get().reactflow.getNodes(); const edges = get().reactflow.getEdges(); @@ -125,6 +128,7 @@ export const generalActionSlice: StateCreator< }, ); }, + deselectAll: () => { get().internalUpdateSelection([], { type: 'selection/deselectAll', payload: {} }); }, @@ -160,6 +164,7 @@ export const generalActionSlice: StateCreator< return selectedElements; }, + paste: async () => { const clipboardItems = await navigator.clipboard.read(); diff --git a/src/FlowEditor/store/slices/nodesSlice.ts b/src/FlowEditor/store/slices/nodesSlice.ts index 469daa1..6fc4769 100644 --- a/src/FlowEditor/store/slices/nodesSlice.ts +++ b/src/FlowEditor/store/slices/nodesSlice.ts @@ -23,7 +23,7 @@ export interface PublicNodesAction { * @param index 要添加到的位置,默认为末尾 */ addNode: (node: Node, index?: number) => void; - addNodes: (nodes: Record, options?: ActionOptions) => void; + addNodes: (nodes: Record | Node[], options?: ActionOptions) => void; /** * 移除指定 id 的节点 * @param id 要移除的节点 id @@ -170,10 +170,17 @@ export const nodesSlice: StateCreator< }, addNodes: (nodes, options) => { + const _nodes = Array.isArray(nodes) + ? nodes.reduce((acc: Record, node) => { + acc[node.id] = node; + return acc; + }, {}) + : nodes; + get().dispatchNodes( { type: 'addNodes', - nodes, + nodes: _nodes, }, options, ); diff --git a/src/FlowView/helper.tsx b/src/FlowView/helper.tsx index 39d175e..823dae9 100644 --- a/src/FlowView/helper.tsx +++ b/src/FlowView/helper.tsx @@ -275,6 +275,8 @@ export const getRenderData = ( const { _nodes, _edges } = setNodePosition(renderNodes, renderEdges, autoLayout, layoutOptions); + console.log(_edges); + return { nodes: _nodes, edges: sortEdges(_edges), diff --git a/src/Input/index.tsx b/src/Input/index.tsx index e573a26..bcc04f8 100644 --- a/src/Input/index.tsx +++ b/src/Input/index.tsx @@ -28,7 +28,7 @@ export interface TextAreaProps extends AntdTextAreaProps { } export const TextArea = memo( - forwardRef(({ className, type = 'ghost', ...props }, ref) => { + forwardRef(({ className, type = 'ghost', ...props }, ref: any) => { const { styles, cx } = useStyles(); return ( diff --git a/src/index.ts b/src/index.ts index 01a48f5..5d571d0 100644 --- a/src/index.ts +++ b/src/index.ts @@ -11,6 +11,7 @@ export { getStraightPath, } from 'reactflow'; export { default as Background } from './Background'; +export { default as SwimlaneBackground } from './Background/components/SwimBg'; export { default as BasicGroupNode } from './BasicGroupNode'; export { default as BasicNode } from './BasicNode'; export { default as CanvasLoading } from './CanvasLoading'; @@ -35,6 +36,15 @@ export * from './Input'; export * from './MiniMap'; export * from './constants'; -export type { Connection, EdgeChange, EdgeProps, NodeChange, NodeProps } from 'reactflow'; +export type { + Connection, + Edge, + EdgeChange, + EdgeProps, + Node, + NodeChange, + NodeProps, +} from 'reactflow'; +export type { SwimLaneProps, SwimlaneBackgroundProps } from './Background/components/SwimBg'; export type { FlowEditorStoreProviderProps } from './FlowStoreProvider'; export type { ExtraAction } from './NodeField'; diff --git a/testDagre.js b/testDagre.js new file mode 100644 index 0000000..5c9c852 --- /dev/null +++ b/testDagre.js @@ -0,0 +1,142 @@ +const Dagre = require('@dagrejs/dagre'); + +const setPosition = (nodes, edges) => { + const g = new Dagre.graphlib.Graph().setDefaultEdgeLabel(() => ({})); + + g.setGraph({ + rankdir: 'LR', + align: 'UL', + nodesep: 100, + ranksep: 200, + }); + + edges.forEach((edge) => g.setEdge(edge.source, edge.target)); + nodes.forEach((node) => g.setNode(node.id, node)); + + Dagre.layout(g); + + g.nodes().forEach(function (v) { + console.log(v + ': ' + g.node(v).x + ', ' + g.node(v).y); + }); +}; + +const nodes1 = [ + { + id: 'b1', + width: 200, + height: 100, + data: { + title: 'SaveMoneyContainer', + description: '直塞发奖弹窗', + logo: 'https://mdn.alipayobjects.com/huamei_fcqe9k/afts/img/A*j8ICSbNgZ3wAAAAAAAAAAAAADtOFAQ/original', + titleSlot: { + type: 'right', + }, + }, + }, + { + id: 'b11', + type: 'Group', + width: 200, + height: 300, + data: { + count: 10, + }, + }, + { + id: 'b2', + type: 'Group', + width: 234, + height: 100, + data: { + count: 2, + }, + }, +]; + +const nodes2 = [ + { + id: 'b1', + width: 200, + height: 100, + data: { + title: 'SaveMoneyContainer', + description: '直塞发奖弹窗', + logo: 'https://mdn.alipayobjects.com/huamei_fcqe9k/afts/img/A*j8ICSbNgZ3wAAAAAAAAAAAAADtOFAQ/original', + titleSlot: { + type: 'right', + }, + }, + }, + { + id: 'b11', + type: 'Group', + width: 200, + height: 400, + data: { + count: 10, + }, + }, + { + id: 'b2', + type: 'Group', + width: 234, + height: 100, + data: { + count: 2, + }, + }, +]; + +const nodes3 = [ + { + id: 'b1', + width: 200, + height: 100, + data: { + title: 'SaveMoneyContainer', + description: '直塞发奖弹窗', + logo: 'https://mdn.alipayobjects.com/huamei_fcqe9k/afts/img/A*j8ICSbNgZ3wAAAAAAAAAAAAADtOFAQ/original', + titleSlot: { + type: 'right', + }, + }, + }, + { + id: 'b11', + type: 'Group', + width: 200, + height: 500, + data: { + count: 10, + }, + }, + { + id: 'b2', + type: 'Group', + width: 234, + height: 100, + data: { + count: 2, + }, + }, +]; + +const edges = [ + { + id: 'b111', + source: 'b1', + target: 'b11', + }, + { + id: 'b12', + source: 'b1', + target: 'b2', + }, +]; + +setPosition(nodes1, edges); + +setPosition(nodes2, edges); + +setPosition(nodes3, edges);