使用阿里lowcode,手搓一个Ant Design的Progress进度条组件

200 阅读4分钟

由于阿里lowcode和alifd/next深度集成,alifd/next的组件样式不满足于现有需求,Ant Design的样式符合现有需求但是需要引入Ant Design,所以想想决定手搓一个。

1、先看效果

20250627_095917.gif

2、上代码

components

ProgressSteps.tsx

import React, {createElement} from "react";
import { Balloon } from '@alifd/next';
import "./index.scss";

export interface ProgressStepsProps {
  steps?: number;
  percent?: number;
  size?: [number, number];
  direction?: 'horizontal' | 'vertical';
  strokeColor?: string;
  trailColor?: string;
  borderRadius?: number;
  width?: string;
  height?: string;
  stepWidth?: number,
  stepHeight?: number,
  labelText?: string,
  labelColor?: string,
  labelSize?: number,
  labelBlod?: boolean,
  percentageColor?: string,
  percentageSize?: number,
  percentageBlod?: boolean,
  isBalloon?: boolean,
  style?: React.CSSProperties;
}

const ProgressSteps: React.FC<ProgressStepsProps> = (props) => {

  const {
    width,
    height,
    steps = 10,
    percent = 50,
    direction = 'horizontal',
    strokeColor = '#1890ff',
    trailColor = '#f0f0f0',
    borderRadius = 2,
    stepWidth = 30,
    stepHeight = 50,
    labelText,
    labelColor,
    labelSize,
    labelBlod,
    percentageColor,
    percentageSize,
    percentageBlod,
    isBalloon,
    style
  } = props;

  const filledSteps = (percent / 100) * steps;

  const isHorizontal = direction === 'horizontal';

  const boxStyle = {
    display: 'flex',
    alignItems: 'center',
    flexDirection: isHorizontal ? 'row' : 'column',
    gap: 2,
    width,
    height,
  }

  const labelStyle = {
    margin: '0 6px',
    color: labelColor,
    fontSize: labelSize,
    fontWeight: labelBlod ? 'bold' : 'normal',
    overflow: "hidden",
    whiteSpace: "nowrap",
    textOverflow: "ellipsis",
  }

  const stepStyle = {
    height: stepHeight,
    width: stepWidth,
    borderRadius,
    transition: 'background 0.3s',
  }

  const percentageStyle = {
    margin: '0 6px',
    color: percentageColor,
    fontSize: percentageSize,
    fontWeight: percentageBlod ? 'bold' : 'normal',
  }

  return (
    <div style={{...boxStyle, ...style}}>
      <div style={labelStyle} title={labelText}>
        {labelText}
      </div>
      {Array.from({ length: steps }).map((_, idx) => {
        let background = trailColor;

        if (idx + 1 <= filledSteps) {
          background = strokeColor;
        } else if (idx < filledSteps) {
          const fillPercent = (filledSteps - idx) * 100;

          background = isHorizontal
            ? `linear-gradient(to right, ${strokeColor} ${fillPercent}%, ${trailColor} 0%)`
            : `linear-gradient(to bottom, ${strokeColor} ${fillPercent}%, ${trailColor} 0%)`;
        }

        return isBalloon ? 
          (<Balloon 
            v2
            trigger={
              <div
                key={idx}
                style={{
                  ...stepStyle,
                  background,
                }}
              />
            } 
            closable={false}
            align="t" 
            triggerType="hover" 
          >
            <div style={{
              fontSize: percentageSize,
              fontWeight: percentageBlod ? 'bold' : 'normal',
            }}>{percent}%</div>
          </Balloon>)
          :
          (<div
            key={idx}
            style={{
              ...stepStyle,
              background,
            }}
          />)
      })}

      <div style={percentageStyle}>
        {percent}%
      </div>
    </div>
  );
};


export default ProgressSteps;

样式scss

.progress-steps-container {
  position: relative;
  width: 100%;
  margin: 20px 0;
}

.progress-steps-background {
  position: absolute;
  top: 50%;
  left: 0;
  width: 100%;
  transform: translateY(-50%);
  background-color: #f0f0f0;
  border-radius: 4px;
}

.progress-steps-foreground {
  position: absolute;
  top: 50%;
  left: 0;
  transform: translateY(-50%);
  background-color: #1890ff;
  border-radius: 4px;
  transition: width 0.3s;
}

.progress-step {
  position: absolute;
  top: 50%;
  transform: translateY(-50%);
  border-radius: 50%;
  text-align: center;
  color: #fff;
  z-index: 1;
}

.progress-step.completed {
  background-color: #1890ff;
}

.progress-step.current {
  background-color: #1890ff;
}

.progress-step.pending {
  background-color: #f0f0f0;
  color: #999;
}

.progress-step.partial {
  background-color: #f0f0f0;
  color: #999;
  overflow: hidden;
  position: relative;
}

.progress-step-partial-fill {
  position: absolute;
  top: 0;
  left: 0;
  bottom: 0;
  background-color: #1890ff;
  z-index: -1;
}
.next-balloon-normal{
  z-index: 9999 !important;
}

lowcode

meta:

import {IPublicTypeComponentMetadata, IPublicTypeSnippet} from '@alilc/lowcode-types';

const ProgressStepsMeta: IPublicTypeComponentMetadata = {
  componentName: 'ProgressSteps',
  title: 'ProgressSteps',
  group: 'xx组件',
  category: 'xx类',
  docUrl: '',
  screenshot: '',
  devMode: 'proCode',
  npm: {
    package: "xxxxxxx",
    version: '0.0.1',
    exportName: 'ProgressSteps',
    main: 'src/index.tsx',
    destructuring: true,
    subName: '',
  },
  configure: {
    props: [
      {
        name: 'legend',
        type: 'group',
        display: 'accordion',
        title: {
          label: '容器',
        },
        items: [
          {
            title: {
              label: {
                type: 'i18n',
                'en-US': 'width',
                'zh-CN': '宽度',
              },
            },
            name: 'width',
            setter: {
              componentName: 'MixedSetter',
              props: {
                setters: [
                  {
                    componentName: 'StringSetter',
                    isRequired: false,
                    initialValue: '',
                  },
                  {
                    componentName: 'NumberSetter',
                    isRequired: false,
                    initialValue: 0,
                  },
                ],
              },
              isRequired: true,
            },
          },
          {
            title: {
              label: {
                type: 'i18n',
                'en-US': 'height',
                'zh-CN': '高度',
              },
            },
            name: 'height',
            setter: {
              componentName: 'MixedSetter',
              props: {
                setters: [
                  {
                    componentName: 'StringSetter',
                    isRequired: false,
                    initialValue: '',
                  },
                  {
                    componentName: 'NumberSetter',
                    isRequired: false,
                    initialValue: 0,
                  },
                ],
              },
              isRequired: true,
            },
          },
        ]
      },
      {
        name: 'legend',
        type: 'group',
        display: 'accordion',
        title: {
          label: '步骤',
        },
        items: [
          {
            title: {
              label: {
                type: 'i18n',
                'en-US': 'steps',
                'zh-CN': '条数',
              },
            },
            name: 'steps',
            setter: {
              componentName: 'NumberSetter',
              isRequired: true,
            },
          },
          {
            title: {
              label: {
                type: 'i18n',
                'en-US': 'stepWidth',
                'zh-CN': '宽度',
              },
            },
            name: 'stepWidth',
            setter: {
              componentName: 'NumberSetter',
              isRequired: true,
            },
          },
          {
            title: {
              label: {
                type: 'i18n',
                'en-US': 'stepHeight',
                'zh-CN': '高度',
              },
            },
            name: 'stepHeight',
            setter: {
              componentName: 'NumberSetter',
              isRequired: true,
            },
          },
          {
            title: {
              label: {
                type: 'i18n',
                'en-US': 'borderRadius',
                'zh-CN': '圆角',
              },
            },
            name: 'borderRadius',
            setter: {
              componentName: 'NumberSetter',
              isRequired: true,
              initialValue: 2,
            },
          },
          {
            title: {
              label: {
                type: 'i18n',
                'en-US': 'percent',
                'zh-CN': '进度',
              },
            },
            name: 'percent',
            setter: {
              componentName: 'NumberSetter',
              isRequired: true,
            },
          },
          {
            title: {
              label: {
                type: 'i18n',
                'en-US': 'strokeColor',
                'zh-CN': '进度颜色',
              },
            },
            name: 'strokeColor',
            setter: {
              componentName: 'ColorSetter',
              isRequired: false,
              initialValue: '#1890ff',
            },
          },
          {
            title: {
              label: {
                type: 'i18n',
                'en-US': 'trailColor',
                'zh-CN': '底层颜色',
              },
            },
            name: 'trailColor',
            setter: {
              componentName: 'ColorSetter',
              isRequired: false,
              initialValue: '#f0f0f0',
            },
          },
        ]
      },
      {
        name: 'legend',
        type: 'group',
        display: 'accordion',
        title: {
          label: '气泡提示',
        },
        items: [
          {
            title: {
              label: {
                type: 'i18n',
                'en-US': 'isBalloon',
                'zh-CN': '显隐',
              },
            },
            name: 'isBalloon',
            setter: {
              componentName: 'BoolSetter',
              isRequired: false,
              initialValue: true,
            },
          },
        ]
      },
      {
        name: 'legend',
        type: 'group',
        display: 'accordion',
        title: {
          label: 'label',
        },
        items: [
          {
            title: {
              label: {
                type: 'i18n',
                'en-US': 'labelText',
                'zh-CN': '内容',
              },
            },
            name: 'labelText',
            setter: {
              componentName: 'StringSetter',
              isRequired: false,
            },
          },
          {
            title: {
              label: {
                type: 'i18n',
                'en-US': 'labelColor',
                'zh-CN': '颜色',
              },
            },
            name: 'labelColor',
            setter: {
              componentName: 'ColorSetter',
              isRequired: false,
              initialValue: '#000',
            },
          },
          {
            title: {
              label: {
                type: 'i18n',
                'en-US': 'labelSize',
                'zh-CN': '字号',
              },
            },
            name: 'labelSize',
            setter: {
              componentName: 'NumberSetter',
              isRequired: false,
              initialValue: 16,
            },
          },
          {
            title: {
              label: {
                type: 'i18n',
                'en-US': 'labelBlod',
                'zh-CN': '加粗',
              },
            },
            name: 'labelBlod',
            setter: {
              componentName: 'BoolSetter',
              isRequired: false,
              initialValue: false,
            },
          },
        ]
      },
      {
        name: 'legend',
        type: 'group',
        display: 'accordion',
        title: {
          label: '百分比',
        },
        items: [
          {
            title: {
              label: {
                type: 'i18n',
                'en-US': 'percentageColor',
                'zh-CN': '颜色',
              },
            },
            name: 'percentageColor',
            setter: {
              componentName: 'ColorSetter',
              isRequired: false,
              initialValue: '#000',
            },
          },
          {
            title: {
              label: {
                type: 'i18n',
                'en-US': 'percentageSize',
                'zh-CN': '字号',
              },
            },
            name: 'percentageSize',
            setter: {
              componentName: 'NumberSetter',
              isRequired: false,
              initialValue: 16,
            },
          },
          {
            title: {
              label: {
                type: 'i18n',
                'en-US': 'percentageBlod',
                'zh-CN': '加粗',
              },
            },
            name: 'percentageBlod',
            setter: {
              componentName: 'BoolSetter',
              isRequired: false,
              initialValue: false,
            },
          },
        ]
      },
    ],
    component: {},
  },
};
const snippets: IPublicTypeSnippet[] = [
  {
    title: '进度条',
    screenshot: '',
    schema: {
      componentName: 'ProgressSteps',
      props: {
        width: '400px',
        height: '60px',
        steps: 10,
        stepWidth: 30,
        stepHeight: 50,
        percent: 50,
        strokeColor: '#1890ff',
        trailColor: '#f0f0f0',
        labelText: 'label',
        labelColor: '#000',
        labelSize: 16,
        labelBlod: false,
        percentageColor: '#000',
        percentageSize: 16,
        percentageBlod: false,
      },
    },
  },
];

export default {
  ...ProgressStepsMeta,
  snippets,
};