43 lines
3.6 KiB
TypeScript
43 lines
3.6 KiB
TypeScript
/**
|
||
* 按名称异步加载画布SVG图标。先读取共享缓存,再订阅图标加载器,展示加载中/失败/已就绪三种图形。
|
||
* 组件负责显示和重试入口;网络并发、缓存以及订阅取消的实现位于canvas-icon-loader。
|
||
*/
|
||
"use client";
|
||
import { createElement, useEffect, useState } from "react";
|
||
import type { LucideProps } from "lucide-react";
|
||
import { loadIcon, peekIcon, type IconPaths } from "@/lib/canvas-icon-loader";
|
||
|
||
/** 兼容Lucide尺寸与描边属性,name默认Star;width/height未提供时跟随size。
|
||
* 成功后按加载器返回的SVG标签与属性创建子元素,避免把整段SVG作为HTML插入。 */
|
||
export function CanvasIcon({ name = "Star", size = 24, width = size, height = size, strokeWidth = 2, absoluteStrokeWidth, ...props }: LucideProps & { name?: string }) {
|
||
// 结果携带图标名,渲染时只采用当前name对应的内容,换名时不会短暂展示上一个图标。
|
||
const [result, setResult] = useState<{ name: string; paths?: IconPaths; error?: boolean }>(() => ({ name, paths: peekIcon(name) }));
|
||
// retry仅用于触发同名图标重新订阅;它不是全局缓存版本或网络请求ID。
|
||
const [retry, setRetry] = useState(0);
|
||
// 换名或点击重试后,先采用可同步取得的缓存,再等待loadIcon。清理时取消本组件订阅;
|
||
// AbortError不显示失败,其它错误显示重试占位。底层决定共享请求是否仍有其它使用者。
|
||
useEffect(() => {
|
||
const controller = new AbortController();
|
||
setResult({ name, paths: peekIcon(name) });
|
||
// 清理通过订阅 signal 取消,但 then 没有额外 active/代次检查;已兑现 Promise 的回调仍可能运行。
|
||
// result.name 只能隔离不同名称,同名重试之间不靠此字段区分代次。
|
||
loadIcon(name, controller.signal).then((paths) => setResult({ name, paths }), (error) => {
|
||
if (error.name !== "AbortError") setResult({ name, error: true });
|
||
});
|
||
return () => controller.abort();
|
||
}, [name, retry]);
|
||
// effect可能还没处理新name,故读取结果前再次比对名称;失败标记也必须属于当前图标。
|
||
const paths = result.name === name ? result.paths : undefined;
|
||
const failed = result.name === name && result.error;
|
||
// retry 不清浏览器 HTTP 缓存;同名已有成功分片时,loadIcon 仍可能直接复用共享缓存。
|
||
// 以24×24为标准坐标;absoluteStrokeWidth在size为数值时按尺寸反向补偿描边宽度。
|
||
// 成功时保留调用方onClick;失败时点击改为停止冒泡并重试,避免同时触发外层节点点击。
|
||
// props 在默认 SVG 属性之后展开,可覆盖 viewBox 等;data-icon-* 和最终 onClick 在其后固定。
|
||
// 加载中用虚线方框,失败用叉号与title,data-icon-state供样式或检查观察显示状态。
|
||
return <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width={width} height={height} fill="none" stroke="currentColor" strokeWidth={absoluteStrokeWidth && typeof size === "number" ? Number(strokeWidth) * 24 / size : strokeWidth} strokeLinecap="round" strokeLinejoin="round"
|
||
{...props} data-icon-id={name} data-icon-state={paths ? "loaded" : failed ? "error" : "loading"}
|
||
onClick={failed ? (event) => { event.stopPropagation(); setRetry((value) => value + 1); } : props.onClick}>
|
||
{paths ? paths.map(([tag, attributes], index) => createElement(tag, { ...attributes, key: attributes.key ?? index })) : failed ? <><title>图标加载失败,点击重试</title><path d="m8 8 8 8m0-8-8 8" opacity=".5" /></> : <rect x="5" y="5" width="14" height="14" rx="3" opacity=".2" strokeDasharray="2 3" />}
|
||
</svg>;
|
||
}
|