Skip to content

图标 ​

安装图标 ​

bash
pnpm install @ant-design/icons@6.x --save

封装一个图片组件 ​

  • components/icon/index.tsx
tsx
import React, { Suspense, useMemo, useState } from "react";
import type {
  ComponentProps,
  ComponentType,
  CSSProperties,
  LazyExoticComponent,
} from "react";
import type { AntdIconProps } from "@ant-design/icons/lib/components/AntdIcon";
import { lazy } from "react";

/** Ant 图标组件类型:支持 className、style、onClick 等通用属性 */
type AntIconComponent = ComponentType<AntdIconProps>;

export interface IconProps {
  /**
   * 支持两种传入方式:
   * 1. 图片 URL 地址,如 https://example.com/icon.png
   * 2. @ant-design/icons 中的图标名称,如 StepBackwardOutlined
   */
  type: string;
  /** 图标尺寸,图片模式下设置宽高,Ant 图标模式下设置 font-size */
  size?: number | string;
  /** 自定义类名 */
  className?: string;
  /** 自定义样式 */
  style?: CSSProperties;
  /** 点击事件 */
  onClick?: ComponentProps<"span">["onClick"];
  /** 图片 alt 文本 */
  alt?: string;
  /** 无效输入或加载失败时的降级占位内容 */
  fallback?: React.ReactNode;
}

/**
 * 判断是否为有效 URL 字符串
 */
const isUrl = (str: string): boolean => {
  try {
    new URL(str);
    return true;
  } catch {
    return false;
  }
};

/**
 * 判断是否为合法的 Ant Design 图标名称(大写字母开头,驼峰命名)
 */
const isValidIconName = (str: string): boolean => {
  return /^[A-Z][a-zA-Z0-9]*$/.test(str);
};

/**
 * 模块级 lazy 组件:在模块顶层完成 lazy() 调用,避免渲染期间创建组件。
 * 内部根据 name 属性动态渲染对应的 @ant-design/icons 图标。
 */
const LazyAntIcon: LazyExoticComponent<
  ComponentType<AntdIconProps & { name: string }>
> = lazy(async () => {
  const icons = await import("@ant-design/icons");
  const map = icons as unknown as Record<string, AntIconComponent>;

  const DynamicIcon: React.FC<AntdIconProps & { name: string }> = ({
    name,
    ...props
  }) => {
    const Component = map[name];
    if (!Component) {
      return null;
    }
    return <Component {...props} />;
  };

  return { default: DynamicIcon };
});

const Icon: React.FC<IconProps> = ({
  type,
  size = 24,
  className,
  style,
  onClick,
  alt = "icon",
  fallback = null,
}) => {
  const mergedStyle: CSSProperties = useMemo(() => {
    const base: CSSProperties = {};
    // 先应用 size 作为默认值
    if (typeof size === "number") {
      base.width = size;
      base.height = size;
      base.fontSize = size;
    } else {
      base.width = size;
      base.height = size;
      base.fontSize = size;
    }
    if (onClick) {
      base.cursor = "pointer";
    }
    // 后展开 style,允许外部显式覆盖 size 设置的默认值
    return { ...base, ...style };
  }, [size, style, onClick]);

  // 记录加载失败的 URL;type 变化时不等即视为未出错,无需 effect 重置
  const [errorUrl, setErrorUrl] = useState<string | null>(null);

  // 解析渲染模式
  const renderMode = useMemo<"url" | "antd" | "invalid">(() => {
    if (isUrl(type)) return "url";
    if (isValidIconName(type)) return "antd";
    return "invalid";
  }, [type]);

  // 模式一:URL 图片渲染
  if (renderMode === "url") {
    if (errorUrl === type) {
      return (
        <span className={className} style={mergedStyle} onClick={onClick}>
          {fallback}
        </span>
      );
    }
    return (
      <img
        src={type}
        alt={alt}
        className={className}
        style={mergedStyle}
        onClick={onClick}
        onError={() => setErrorUrl(type)}
      />
    );
  }

  // 模式二:Ant Design 图标渲染
  if (renderMode === "antd") {
    return (
      <Suspense
        fallback={
          <span className={className} style={mergedStyle} onClick={onClick}>
            {fallback}
          </span>
        }
      >
        <LazyAntIcon
          name={type}
          className={className}
          style={mergedStyle}
          onClick={onClick}
        />
      </Suspense>
    );
  }

  // 无效输入降级展示
  return (
    <span className={className} style={mergedStyle} onClick={onClick}>
      {fallback}
    </span>
  );
};

export default Icon;

使用 ​

tsx
import Icon from "@/components/icon";

function LoginPage() {
  return (
    <div>
      <div>
        <Icon
          type="StepBackwardOutlined"
          style={{
            width: "128px",
            height: "128px",
            color: "red",
            fontSize: "128px",
          }}
        />
        Login
      </div>
    </div>
  );
}

export default LoginPage;