index.tsx 3.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596
  1. import React from 'react';
  2. type StepStatus = 'wait' | 'process' | 'finish';
  3. /**
  4. * StepItem 用于描述步骤条的单个步骤信息
  5. */
  6. export interface StepItem {
  7. /** 步骤标题 */
  8. title: string;
  9. /** 步骤的说明文本(可选) */
  10. description?: string;
  11. /** 步骤序号(可选) */
  12. number?: number;
  13. /** 步骤状态,可选: wait | process | finish,默认为 wait */
  14. status?: StepStatus;
  15. /** 可自定义扩展内容(可选) */
  16. extra?: React.ReactNode;
  17. }
  18. export interface StepProps {
  19. steps: StepItem[];
  20. className?: string;
  21. showArrow?: boolean;
  22. onStepClick?: (step: StepItem, index: number) => void;
  23. }
  24. const gradientPresets = [
  25. 'bg-gradient-to-r from-[#95D3F4] to-white',
  26. 'bg-gradient-to-r from-[#9ABEFF] to-white',
  27. 'bg-gradient-to-r from-[#87CEFA] to-white',
  28. ];
  29. const statusRingMap: Record<StepStatus, string> = {
  30. wait: 'ring-1 ring-slate-200',
  31. process: 'ring-2 ring-[#4195E5]',
  32. finish: 'ring-2 ring-emerald-300',
  33. };
  34. const statusTitleMap: Record<StepStatus, string> = {
  35. wait: 'text-gray-900',
  36. process: 'text-[#1d5fbf]',
  37. finish: 'text-emerald-700',
  38. };
  39. const cx = (...classNames: Array<string | false | undefined>) => classNames.filter(Boolean).join(' ');
  40. const Step: React.FC<StepProps> = ({ steps, className, showArrow = true, onStepClick }) => {
  41. if (!steps?.length) {
  42. return null;
  43. }
  44. return (
  45. <div className={cx('flex items-center w-full', className)}>
  46. {steps.map((step, index) => {
  47. const status = step.status ?? 'wait';
  48. const gradient = gradientPresets[index % gradientPresets.length];
  49. const card = (
  50. <div
  51. key={step.title ?? index}
  52. className={cx(
  53. 'relative h-[100px] flex-1 rounded-xl p-6 shadow-md transition-all duration-200 flex flex-col justify-center',
  54. gradient,
  55. statusRingMap[status],
  56. onStepClick && 'cursor-pointer hover:-translate-y-1',
  57. )}
  58. onClick={() => onStepClick?.(step, index)}
  59. >
  60. <div className="absolute top-3 right-4 text-right italic">
  61. <div className="text-3xl font-bold text-[#4195E5]">{step.number ?? index + 1}</div>
  62. <div className="text-xs font-medium text-[#4195E5] text-opacity-80 uppercase">step</div>
  63. </div>
  64. <p className={cx('mb-2 text-lg font-semibold', statusTitleMap[status])}>{step.title}</p>
  65. {step.description && (
  66. <p className="text-sm leading-relaxed text-gray-600 w-[80%]">{step.description}</p>
  67. )}
  68. {step.extra && <div className="mt-3">{step.extra}</div>}
  69. </div>
  70. );
  71. return (
  72. <React.Fragment key={`${step.title}-${index}`}>
  73. {card}
  74. {showArrow && index < steps.length - 1 && (
  75. <div className="mx-3 text-lg font-semibold text-gray-400">{'>'}</div>
  76. )}
  77. </React.Fragment>
  78. );
  79. })}
  80. </div>
  81. );
  82. };
  83. Step.displayName = 'Step';
  84. export default Step;