图标
安装图标
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;