Files
Endoleap/src/components/canvas/canvas-icon.tsx
T

43 lines
3.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 按名称异步加载画布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>;
}