|
@@ -0,0 +1,453 @@
|
|
|
+import { CompoundedComponent } from "@/types";
|
|
|
+import { register } from "@antv/x6-react-shape";
|
|
|
+import { Graph, Node } from "@antv/x6";
|
|
|
+import { defaultData } from "../data";
|
|
|
+import CustomInput from "../CustomInput";
|
|
|
+import { useSizeHook, useShapeProps } from "@/hooks";
|
|
|
+import { useEffect, useRef, useState } from "react";
|
|
|
+import Setting from "./setting";
|
|
|
+import { LaneItem, StageItem } from "@/types";
|
|
|
+
|
|
|
+const component = ({ node, graph }: { node: Node; graph: Graph }) => {
|
|
|
+ const {
|
|
|
+ poolName,
|
|
|
+ text,
|
|
|
+ fill,
|
|
|
+ stroke,
|
|
|
+ opacity,
|
|
|
+ lane,
|
|
|
+ stage,
|
|
|
+ direction,
|
|
|
+ textDirection,
|
|
|
+ headerHeight,
|
|
|
+ stageWidth,
|
|
|
+ laneHeadHeight,
|
|
|
+ } = node.getData();
|
|
|
+ const [showSetting, setShowSetting] = useState(false);
|
|
|
+ const { size, ref } = useSizeHook();
|
|
|
+ const lister = useRef<any>();
|
|
|
+ const { fillContent, strokeColor, strokeWidth } = useShapeProps(
|
|
|
+ fill,
|
|
|
+ size,
|
|
|
+ stroke
|
|
|
+ );
|
|
|
+
|
|
|
+ graph.on("node:selected", (args) => {
|
|
|
+ setShowSetting(
|
|
|
+ graph.getSelectedCells().length === 1 && node.id === args.node.id
|
|
|
+ );
|
|
|
+ });
|
|
|
+ graph.on("node:unselected", (args) => {
|
|
|
+ if (node.id === args.node.id) setShowSetting(false);
|
|
|
+ });
|
|
|
+
|
|
|
+ useEffect(() => {
|
|
|
+ let width = node.getSize().width;
|
|
|
+ let height = node.getSize().height;
|
|
|
+
|
|
|
+ if (lane.length) {
|
|
|
+ height = lane.reduce(
|
|
|
+ (a: number, b: LaneItem) => a + b.width + strokeWidth,
|
|
|
+ 0
|
|
|
+ );
|
|
|
+ }
|
|
|
+ if (stage.length) {
|
|
|
+ height += stageWidth;
|
|
|
+ }
|
|
|
+
|
|
|
+ node.setSize({ width, height });
|
|
|
+ }, [lane.length]);
|
|
|
+
|
|
|
+ // 监听宽高变化
|
|
|
+ if (!lister.current) {
|
|
|
+ lister.current = node.on("change:size", (args) => {
|
|
|
+ const lane = node.data.lane;
|
|
|
+ const stage = node.data.stage;
|
|
|
+ // 更新泳道宽度
|
|
|
+ if (lane.length) {
|
|
|
+ const originWidth = lane.reduce(
|
|
|
+ (a: number, b: LaneItem) => a + b.width,
|
|
|
+ 0
|
|
|
+ );
|
|
|
+ const offsetY =
|
|
|
+ (args?.current?.height || 0) -
|
|
|
+ originWidth -
|
|
|
+ (stage.length ? stageWidth : 0);
|
|
|
+
|
|
|
+ if (offsetY) {
|
|
|
+ node.setData({
|
|
|
+ lane: lane.map((item: LaneItem) => {
|
|
|
+ return {
|
|
|
+ ...item,
|
|
|
+ width: item.width + offsetY / lane.length - strokeWidth,
|
|
|
+ };
|
|
|
+ }),
|
|
|
+ });
|
|
|
+ }
|
|
|
+ }
|
|
|
+ // 更新阶段高度
|
|
|
+ if (stage.length) {
|
|
|
+ const originHeight = stage.reduce(
|
|
|
+ (a: number, b: StageItem) => a + b.height,
|
|
|
+ 0
|
|
|
+ );
|
|
|
+ const offsetX =
|
|
|
+ (args?.current?.width || 0) -
|
|
|
+ originHeight - headerHeight;
|
|
|
+
|
|
|
+ if (offsetX) {
|
|
|
+ node.setData({
|
|
|
+ stage: stage.map((item: StageItem) => {
|
|
|
+ return {
|
|
|
+ ...item,
|
|
|
+ height: item.height + offsetX / stage.length - strokeWidth,
|
|
|
+ };
|
|
|
+ }),
|
|
|
+ });
|
|
|
+ }
|
|
|
+ }
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ const handleStartMove = () => {
|
|
|
+ node.setData({
|
|
|
+ ignoreDrag: false,
|
|
|
+ });
|
|
|
+ };
|
|
|
+
|
|
|
+ const handleEndMove = () => {
|
|
|
+ node.setData({
|
|
|
+ ignoreDrag: true,
|
|
|
+ });
|
|
|
+ };
|
|
|
+
|
|
|
+ // 插入阶段
|
|
|
+ const handleInsertStage = (x: number, y: number) => {
|
|
|
+ const stage = node.data.stage || [];
|
|
|
+ const { width, height } = node.getSize();
|
|
|
+ // 新阶段位置
|
|
|
+ const w1 = x - node.getPosition().x - headerHeight;
|
|
|
+ // 无阶段 从中切开
|
|
|
+ if (!stage.length) {
|
|
|
+ const w2 = width - w1 - headerHeight;
|
|
|
+ node.setData({
|
|
|
+ stage: [
|
|
|
+ {
|
|
|
+ name: "阶段",
|
|
|
+ height: w1,
|
|
|
+ },
|
|
|
+ {
|
|
|
+ name: "阶段",
|
|
|
+ height: w2,
|
|
|
+ },
|
|
|
+ ],
|
|
|
+ });
|
|
|
+ node.setSize({
|
|
|
+ width,
|
|
|
+ height: height + stageWidth
|
|
|
+ })
|
|
|
+ } else {
|
|
|
+ // 有阶段 则找出在哪一段内 再分成两段
|
|
|
+ let temp = 0; // 记录长度
|
|
|
+ for (let i = 0; i < stage.length; i++) {
|
|
|
+ const item = stage[i];
|
|
|
+ temp += item.height;
|
|
|
+ if (temp > w1) {
|
|
|
+ // 插入
|
|
|
+ const w3 = temp - w1;
|
|
|
+ const w4 = item.height - w3;
|
|
|
+ const newStages = [
|
|
|
+ {
|
|
|
+ name: "阶段",
|
|
|
+ height: w4,
|
|
|
+ },
|
|
|
+ {
|
|
|
+ ...item,
|
|
|
+ height: w3,
|
|
|
+ },
|
|
|
+ ];
|
|
|
+ stage.splice(i, 1, ...newStages);
|
|
|
+ node.setData(
|
|
|
+ {
|
|
|
+ stage: [...stage],
|
|
|
+ },
|
|
|
+ {
|
|
|
+ deep: false,
|
|
|
+ }
|
|
|
+ );
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ // 插入泳道
|
|
|
+ const handleInsertLane = (x: number, _y: number) => {
|
|
|
+ console.log('插入泳道');
|
|
|
+ const lane = node.data.lane || [];
|
|
|
+ const stage = node.data.stage || [];
|
|
|
+ if(!lane.length) {
|
|
|
+ node.setData({
|
|
|
+ lane: [{
|
|
|
+ name: "",
|
|
|
+ width: node.getSize().height - (stage.length ? stageWidth : 0)
|
|
|
+ }]
|
|
|
+ })
|
|
|
+ } else {
|
|
|
+ // 判断在哪一个泳道内
|
|
|
+ let temp = 0;
|
|
|
+ const w1 = x - node.getPosition().y;
|
|
|
+ for (let i = 0; i < lane.length; i++) {
|
|
|
+ const item = lane[i];
|
|
|
+ temp += item.width;
|
|
|
+ if (temp > w1) {
|
|
|
+ // 插入
|
|
|
+ lane.splice(i, 0, {
|
|
|
+ name: "",
|
|
|
+ width: lane[lane.length - 1].width
|
|
|
+ })
|
|
|
+ node.setData({
|
|
|
+ lane: [...lane]
|
|
|
+ });
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ const listerEmbedded = useRef<any>();
|
|
|
+ if (!listerEmbedded.current) {
|
|
|
+ listerEmbedded.current = graph.on("node:embedded", (args) => {
|
|
|
+ const {isSeparator, isLane, separatorDiration, laneDirection } = args.node.data || {};
|
|
|
+ // 分隔符
|
|
|
+ if (isSeparator && separatorDiration !== direction) {
|
|
|
+ const bbox = node.getBBox();
|
|
|
+ if (bbox.isIntersectWithRect(args.node.getBBox())) {
|
|
|
+ handleInsertStage(args.x, args.y);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ // 泳道
|
|
|
+ if (isLane && laneDirection === direction) {
|
|
|
+ const bbox = node.getBBox();
|
|
|
+ if (bbox.isIntersectWithRect(args.node.getBBox())) {
|
|
|
+ handleInsertLane(args.x, args.y);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ const handleChangeLane = (val: number | null) => {
|
|
|
+ if (!val) {
|
|
|
+ node.setData({ lane: [] }, { deep: false });
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ const currentLanes = node.data.lane || [];
|
|
|
+ const currentLength = currentLanes.length;
|
|
|
+
|
|
|
+ if (currentLength < val) {
|
|
|
+ let width = node.getSize().width;
|
|
|
+ if (currentLength > 0) {
|
|
|
+ width = currentLanes[currentLength - 1].width;
|
|
|
+ }
|
|
|
+ const newLanes = new Array(val - currentLength).fill({ width, name: "" });
|
|
|
+ node.setData({ lane: [...(node.data.lane || []), ...newLanes] });
|
|
|
+ } else {
|
|
|
+ node.updateData({
|
|
|
+ lane: currentLanes.slice(0, val),
|
|
|
+ });
|
|
|
+ }
|
|
|
+ };
|
|
|
+ return (
|
|
|
+ <>
|
|
|
+ {showSetting && <Setting node={node} onChangeLane={handleChangeLane} />}
|
|
|
+ <div
|
|
|
+ className="relative text-0 w-full h-full flex"
|
|
|
+ style={{
|
|
|
+ opacity: opacity / 100,
|
|
|
+ border: `solid ${strokeWidth}px ${strokeColor}`,
|
|
|
+ }}
|
|
|
+ ref={ref}
|
|
|
+ >
|
|
|
+ <div
|
|
|
+ className="h-full relative cursor-move"
|
|
|
+ style={{
|
|
|
+ width: headerHeight,
|
|
|
+ background: fillContent,
|
|
|
+ borderRight: `solid ${strokeWidth}px ${strokeColor}`,
|
|
|
+ }}
|
|
|
+ onMouseEnter={handleStartMove}
|
|
|
+ onMouseLeave={handleEndMove}
|
|
|
+ >
|
|
|
+ <CustomInput
|
|
|
+ value={poolName}
|
|
|
+ styles={text}
|
|
|
+ node={node}
|
|
|
+ onChange={(val) => node.setData({ poolName: val })}
|
|
|
+ txtStyle={{
|
|
|
+ transform: `rotate(-90deg) translateX(-${size.height}px)`,
|
|
|
+ transformOrigin: '0 0',
|
|
|
+ width: size.height,
|
|
|
+ height: headerHeight,
|
|
|
+ }}
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ <div
|
|
|
+ className="relative content h-full"
|
|
|
+ style={{ width: `calc(100% - ${headerHeight}px)` }}
|
|
|
+ >
|
|
|
+ {/* 阶段 */}
|
|
|
+ {stage.length ? (
|
|
|
+ <div
|
|
|
+ className="stage h-full w-full absolute top-0 left-0 flex"
|
|
|
+ >
|
|
|
+ {stage.map((stageItem: StageItem, index: number) => {
|
|
|
+ return (
|
|
|
+ <div key={index}
|
|
|
+ style={{
|
|
|
+ width: stageItem.height - strokeWidth,
|
|
|
+ height: '100%',
|
|
|
+ borderRight:
|
|
|
+ index < stage.length - 1
|
|
|
+ ? `solid ${strokeWidth}px ${strokeColor}`
|
|
|
+ : "node",
|
|
|
+ }}
|
|
|
+ >
|
|
|
+ <div
|
|
|
+ className="relative stage-item cursor-move"
|
|
|
+ style={{
|
|
|
+ width: stageItem.height - 2 * strokeWidth,
|
|
|
+ background: fillContent,
|
|
|
+ height: stageWidth,
|
|
|
+ borderBottom: `solid ${strokeWidth}px ${strokeColor}`,
|
|
|
+ }}
|
|
|
+ onMouseEnter={handleStartMove}
|
|
|
+ onMouseLeave={handleEndMove}
|
|
|
+ >
|
|
|
+ <CustomInput
|
|
|
+ value={stageItem.name}
|
|
|
+ styles={text}
|
|
|
+ node={node}
|
|
|
+ onChange={(val) => {
|
|
|
+ node.setData({
|
|
|
+ stage: stage.map((item: StageItem, i: number) => {
|
|
|
+ if (index === i) {
|
|
|
+ return {
|
|
|
+ ...item,
|
|
|
+ name: val,
|
|
|
+ };
|
|
|
+ }
|
|
|
+ }),
|
|
|
+ });
|
|
|
+ }}
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ );
|
|
|
+ })}
|
|
|
+ </div>
|
|
|
+ ) : (
|
|
|
+ ""
|
|
|
+ )}
|
|
|
+ {/* 泳道 */}
|
|
|
+ <div className="lane absolute left-0" style={{
|
|
|
+ top: stage.length ? stageWidth : 0,
|
|
|
+ width: `100%`,
|
|
|
+ height: `calc(100% - ${stage.length ? stageWidth : 0}px)`
|
|
|
+ }}>
|
|
|
+ {lane.map((item: LaneItem, index: number) => (
|
|
|
+ <div
|
|
|
+ key={index}
|
|
|
+ style={{
|
|
|
+ width: '100%',
|
|
|
+ height: item.width,
|
|
|
+ borderBottom:
|
|
|
+ index === lane.length - 1
|
|
|
+ ? "none"
|
|
|
+ : `solid ${strokeWidth}px ${strokeColor}`,
|
|
|
+ }}
|
|
|
+ >
|
|
|
+ <div
|
|
|
+ className="flex-1 w-full relative cursor-move"
|
|
|
+ style={{
|
|
|
+ width: laneHeadHeight,
|
|
|
+ height: '100%',
|
|
|
+ background: fillContent,
|
|
|
+ borderRight: `solid ${strokeWidth}px ${strokeColor}`,
|
|
|
+ }}
|
|
|
+ onMouseEnter={handleStartMove}
|
|
|
+ onMouseLeave={handleEndMove}
|
|
|
+ >
|
|
|
+ <CustomInput
|
|
|
+ value={item.name}
|
|
|
+ styles={text}
|
|
|
+ node={node}
|
|
|
+ txtStyle={{
|
|
|
+ transform: `rotate(-90deg) translateX(-${item.width}px)`,
|
|
|
+ transformOrigin: '0 0',
|
|
|
+ width: item.width - strokeWidth,
|
|
|
+ height: laneHeadHeight,
|
|
|
+ }}
|
|
|
+ onChange={(val) => {
|
|
|
+ node.setData({
|
|
|
+ lane: lane.map((item: LaneItem, i: number) => {
|
|
|
+ if (index === i) {
|
|
|
+ return {
|
|
|
+ ...item,
|
|
|
+ name: val,
|
|
|
+ };
|
|
|
+ }
|
|
|
+ return item;
|
|
|
+ }),
|
|
|
+ });
|
|
|
+ }}
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ ))}
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </>
|
|
|
+ );
|
|
|
+};
|
|
|
+
|
|
|
+register({
|
|
|
+ shape: "custom-react-horizontalPool",
|
|
|
+ width: 540,
|
|
|
+ height: 250,
|
|
|
+ effect: ["data"],
|
|
|
+ component: component,
|
|
|
+});
|
|
|
+
|
|
|
+const horizontalPool: CompoundedComponent = {
|
|
|
+ name: "泳池(垂直)",
|
|
|
+ icon: require("./image/horizontalPool.png"),
|
|
|
+ node: {
|
|
|
+ shape: "custom-react-horizontalPool",
|
|
|
+ data: {
|
|
|
+ label: "",
|
|
|
+ // 泳池名称
|
|
|
+ poolName: "",
|
|
|
+ // 泳道
|
|
|
+ lane: [],
|
|
|
+ // 阶段
|
|
|
+ stage: [],
|
|
|
+ // 泳道名称高度
|
|
|
+ headerHeight: 40,
|
|
|
+ // 阶段宽度
|
|
|
+ stageWidth: 20,
|
|
|
+ // 泳道名称高度
|
|
|
+ laneHeadHeight: 40,
|
|
|
+ // 泳池方向
|
|
|
+ direction: "horizontal",
|
|
|
+ // 文字方向
|
|
|
+ textDirection: "vertical",
|
|
|
+ // 忽略拖拽
|
|
|
+ ignoreDrag: true,
|
|
|
+ // 其他
|
|
|
+ ...defaultData,
|
|
|
+ },
|
|
|
+ },
|
|
|
+};
|
|
|
+
|
|
|
+export default horizontalPool;
|