From f7eb116aa3d853763d93984fc0c7d5aa5610fa7a Mon Sep 17 00:00:00 2001 From: hypercross Date: Thu, 26 Feb 2026 18:11:33 +0800 Subject: [PATCH] feat: a pin component --- src/components/Article.tsx | 2 +- src/components/index.ts | 4 +- src/components/md-pin.tsx | 146 +++++++++++++++++++++++++++++++++++++ 3 files changed, 149 insertions(+), 3 deletions(-) create mode 100644 src/components/md-pin.tsx diff --git a/src/components/Article.tsx b/src/components/Article.tsx index 6844ea0..7c0bd3a 100644 --- a/src/components/Article.tsx +++ b/src/components/Article.tsx @@ -48,7 +48,7 @@ export const Article: Component = (props) => {
加载失败:{content.error?.message}
-
+
); diff --git a/src/components/index.ts b/src/components/index.ts index e479273..2b26e1a 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -2,6 +2,7 @@ import './dice'; import './table'; import './md-link'; +import './md-pin'; // 导出组件 export { Article } from './Article'; @@ -12,5 +13,4 @@ export { FileTreeNode, HeadingNode } from './FileTree'; // 导出数据类型 export type { DiceProps } from './dice'; -export type { TableProps } from './table'; - +export type { TableProps } from './table'; \ No newline at end of file diff --git a/src/components/md-pin.tsx b/src/components/md-pin.tsx new file mode 100644 index 0000000..6a17a41 --- /dev/null +++ b/src/components/md-pin.tsx @@ -0,0 +1,146 @@ +import { customElement, noShadowDOM } from "solid-element"; +import { createSignal, onMount, onCleanup, Show } from "solid-js"; + +customElement("md-pin", { x: 0, y: 0 }, (props, { element }) => { + noShadowDOM(); + + const [position, setPosition] = createSignal<{ top: string; left: string }>({ top: "0", left: "0" }); + const [visible, setVisible] = createSignal(false); + const [containerStyle, setContainerStyle] = createSignal<{ position: string; top: string; left: string; width: string; height: string }>({ + position: "absolute", + top: "0", + left: "0", + width: "0", + height: "0" + }); + let pinContainer: HTMLSpanElement | undefined; + let targetImage: HTMLImageElement | undefined; + let resizeObserver: ResizeObserver | undefined; + + // 从 element 的 textContent 获取 pin 标签内容 + const label = element?.textContent?.trim() || ""; + + // 隐藏原始文本内容 + if (element) { + element.textContent = ""; + } + + // 查找上方最近的图片 + const findNearestImage = (): HTMLImageElement | null => { + if (!element) return null; + + // 从当前元素向上查找 + let current: Element | null = element; + while (current) { + // 在当前元素的之前兄弟节点中查找图片 + let sibling: Element | null = current.previousElementSibling; + while (sibling) { + // 检查是否是图片元素 + const img = sibling.querySelector('img'); + if (img) return img; + + // 检查元素本身是否有图片相关的类或标签 + if (sibling.tagName === 'IMG') return sibling as HTMLImageElement; + + sibling = sibling.previousElementSibling; + } + current = current.parentElement; + } + + return null; + }; + + // 更新 pin 位置和容器样式 + const updatePosition = () => { + if (!targetImage || !pinContainer) return; + + const imgRect = targetImage.getBoundingClientRect(); + const articleEl = element?.closest('article[data-src]'); + const articleRect = articleEl?.getBoundingClientRect(); + + if (!articleRect) return; + + // 计算图片相对于 article 的位置 + const relativeTop = imgRect.top - articleRect.top; + const relativeLeft = imgRect.left - articleRect.left; + + // 设置容器样式,使其定位到图片位置 + setContainerStyle({ + position: "absolute", + top: `${relativeTop}px`, + left: `${relativeLeft}px`, + width: `${imgRect.width}px`, + height: `${imgRect.height}px` + }); + + // 计算 pin 在图片内的相对位置(x/y 是百分比) + const x = typeof props.x === 'number' ? props.x : parseFloat(props.x) || 0; + const y = typeof props.y === 'number' ? props.y : parseFloat(props.y) || 0; + + const left = (x / 100) * imgRect.width; + const top = (y / 100) * imgRect.height; + + setPosition({ + left: `${left}px`, + top: `${top}px` + }); + }; + + onMount(() => { + // 查找目标图片 + targetImage = findNearestImage(); + + if (targetImage) { + // 确保图片容器是 relative 定位 + const imgParent = targetImage.parentElement; + if (imgParent) { + const parentStyle = window.getComputedStyle(imgParent); + if (parentStyle.position === 'static') { + imgParent.style.position = 'relative'; + } + } + + // 初始定位 + updatePosition(); + + // 使用 ResizeObserver 监听图片大小变化 + resizeObserver = new ResizeObserver(() => { + updatePosition(); + }); + resizeObserver.observe(targetImage); + + // 延迟显示以等待位置计算完成 + requestAnimationFrame(() => { + setVisible(true); + }); + } else { + console.warn('md-pin: 未找到目标图片'); + } + }); + + onCleanup(() => { + if (resizeObserver && targetImage) { + resizeObserver.unobserve(targetImage); + } + }); + + return ( + + + + {label || '📍'} + + + + ); +});