port.tsx 3.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117
  1. /**
  2. * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
  3. * SPDX-License-Identifier: MIT
  4. */
  5. import React, { useEffect, useState } from 'react';
  6. import {
  7. useService,
  8. WorkflowHoverService,
  9. WorkflowLinesManager,
  10. WorkflowPortEntity,
  11. } from '@flowgram.ai/free-layout-editor';
  12. import { NodeColorMap } from './node-color';
  13. export interface WorkflowPortRenderProps {
  14. entity: WorkflowPortEntity;
  15. className?: string;
  16. style?: React.CSSProperties;
  17. onClick?: (e: React.MouseEvent<HTMLDivElement>, port: WorkflowPortEntity) => void;
  18. /** 激活状态颜色 (linked/hovered) */
  19. primaryColor?: string;
  20. /** 默认状态颜色 */
  21. secondaryColor?: string;
  22. /** 错误状态颜色 */
  23. errorColor?: string;
  24. /** 背景颜色 */
  25. backgroundColor?: string;
  26. }
  27. export const PortRender: React.FC<WorkflowPortRenderProps> =
  28. // eslint-disable-next-line react/display-name
  29. React.memo<WorkflowPortRenderProps>((props: WorkflowPortRenderProps) => {
  30. const hoverService = useService<WorkflowHoverService>(WorkflowHoverService);
  31. const { entity } = props;
  32. const { relativePosition } = entity;
  33. const [targetElement, setTargetElement] = useState(entity.targetElement);
  34. const [posX, updatePosX] = useState(relativePosition.x);
  35. const [posY, updatePosY] = useState(relativePosition.y);
  36. const [hovered, setHovered] = useState(false);
  37. useEffect(() => {
  38. // useEffect 时序问题可能导致 port.hasError 非最新,需重新触发一次 validate
  39. entity.validate();
  40. const dispose = entity.onEntityChange(() => {
  41. // 如果有挂载的节点,不需要更新位置信息
  42. if (entity.targetElement) {
  43. if (entity.targetElement !== targetElement) {
  44. setTargetElement(entity.targetElement);
  45. }
  46. return;
  47. }
  48. const newPos = entity.relativePosition;
  49. // 加上 round 避免点位抖动
  50. updatePosX(Math.round(newPos.x));
  51. updatePosY(Math.round(newPos.y));
  52. });
  53. const dispose2 = hoverService.onHoveredChange((id) => {
  54. setHovered(hoverService.isHovered(entity.id));
  55. });
  56. return () => {
  57. dispose.dispose();
  58. dispose2.dispose();
  59. };
  60. }, [hoverService, entity, targetElement]);
  61. // 构建 CSS 自定义属性用于颜色覆盖
  62. const colorStyles: Record<string, string> = {};
  63. if (props.primaryColor) {
  64. colorStyles['--g-workflow-port-color-primary'] = props.primaryColor;
  65. }
  66. if (props.secondaryColor) {
  67. colorStyles['--g-workflow-port-color-secondary'] = props.secondaryColor;
  68. }
  69. if (props.errorColor) {
  70. colorStyles['--g-workflow-port-color-error'] = props.errorColor;
  71. }
  72. if (props.backgroundColor) {
  73. colorStyles['--g-workflow-port-color-background'] = props.backgroundColor;
  74. }
  75. const content = (
  76. <div
  77. style={{
  78. width: '24px',
  79. height: '24px',
  80. borderRadius: '50%',
  81. marginTop: '-12px',
  82. marginLeft: '-12px',
  83. position: 'absolute',
  84. background: '#fff',
  85. border: 'none',
  86. display: 'flex',
  87. alignItems: 'center',
  88. justifyContent: 'center',
  89. cursor: 'pointer',
  90. left: posX,
  91. top: posY,
  92. }}
  93. data-port-entity-id={entity.id}
  94. data-port-entity-type={entity.portType}
  95. data-testid="sdk.workflow.canvas.node.port"
  96. >
  97. <div
  98. style={{
  99. width: hovered ? '20px' : '16px',
  100. height: hovered ? '20px' : '16px',
  101. borderRadius: '50%',
  102. background: NodeColorMap[entity.node.id] ?? '#fff',
  103. transition: 'width 0.2s ease, height 0.2s ease',
  104. }}
  105. />
  106. </div>
  107. );
  108. return content;
  109. });